diff --git a/AUTHORS.md b/AUTHORS.md index 9791171b9c..e2903febe4 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -245,6 +245,7 @@ Programmers xyzz Yohaulticetl Yuri Krupenin + Yury Stepovikov zelurker Documentation diff --git a/CHANGELOG.md b/CHANGELOG.md index 656299fec5..b014ca0389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,10 @@ Bug #4754: Stack of ammunition cannot be equipped partially Bug #4816: GetWeaponDrawn returns 1 before weapon is attached Bug #4822: Non-weapon equipment and body parts can't inherit time from parent animation + Bug #4898: Odd/Incorrect lighting on meshes Bug #5057: Weapon swing sound plays at same pitch whether it hits or misses Bug #5062: Root bone rotations for NPC animation don't work the same as for creature animation + Bug #5065: Actors with scripted animation still try to wander and turn around without moving Bug #5066: Quirks with starting and stopping scripted animations Bug #5129: Stuttering animation on Centurion Archer Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place @@ -32,6 +34,7 @@ Bug #6190: Unintuitive sun specularity time of day dependence Bug #6222: global map cell size can crash openmw if set to too high a value Bug #6313: Followers with high Fight can turn hostile + Bug #6402: The sound of a thunderstorm does not stop playing after entering the premises Bug #6427: Enemy health bar disappears before damaging effect ends Bug #6550: Cloned body parts don't inherit texture effects Bug #6645: Enemy block sounds align with animation instead of blocked hits @@ -127,6 +130,9 @@ Bug #7742: Governing attribute training limit should use the modified attribute Bug #7758: Water walking is not taken into account to compute path cost on the water Bug #7761: Rain and ambient loop sounds are mutually exclusive + Bug #7765: OpenMW-CS: Touch Record option is broken + Bug #7770: Sword of the Perithia: Script execution failure + Bug #7780: Non-ASCII texture paths in NIF files don't work Feature #2566: Handle NAM9 records for manual cell references Feature #3537: Shader-based water ripples Feature #5173: Support for NiFogProperty diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp index 7d87899f4a..504aef7e67 100644 --- a/apps/bulletobjecttool/main.cpp +++ b/apps/bulletobjecttool/main.cpp @@ -174,7 +174,7 @@ namespace constexpr double expiryDelay = 0; Resource::ImageManager imageManager(&vfs, expiryDelay); - Resource::NifFileManager nifFileManager(&vfs); + Resource::NifFileManager nifFileManager(&vfs, &encoder.getStatelessEncoder()); Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, expiryDelay); Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); diff --git a/apps/essimporter/convertacdt.cpp b/apps/essimporter/convertacdt.cpp index 8342310cf6..a737e0a3a2 100644 --- a/apps/essimporter/convertacdt.cpp +++ b/apps/essimporter/convertacdt.cpp @@ -85,7 +85,7 @@ namespace ESSImport Misc::StringUtils::lowerCaseInPlace(group); ESM::AnimationState::ScriptedAnimation scriptedAnim; - scriptedAnim.mGroup = group; + scriptedAnim.mGroup = std::move(group); scriptedAnim.mTime = anis.mTime; scriptedAnim.mAbsolute = true; // Neither loop count nor queueing seems to be supported by the ess format. diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp index 07146fc388..4c4bd1e438 100644 --- a/apps/essimporter/converter.cpp +++ b/apps/essimporter/converter.cpp @@ -306,12 +306,12 @@ namespace ESSImport mMarkers.push_back(marker); } - newcell.mRefs = cellrefs; + newcell.mRefs = std::move(cellrefs); if (cell.isExterior()) - mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = newcell; + mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = std::move(newcell); else - mIntCells[cell.mName] = newcell; + mIntCells[cell.mName] = std::move(newcell); } void ConvertCell::writeCell(const Cell& cell, ESM::ESMWriter& esm) diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index b360c215e6..735bcf1df1 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -34,7 +34,6 @@ Launcher::GraphicsPage::GraphicsPage(QWidget* parent) connect(standardRadioButton, &QRadioButton::toggled, this, &GraphicsPage::slotStandardToggled); connect(screenComboBox, qOverload(&QComboBox::currentIndexChanged), this, &GraphicsPage::screenChanged); connect(framerateLimitCheckBox, &QCheckBox::toggled, this, &GraphicsPage::slotFramerateLimitToggled); - connect(shadowDistanceCheckBox, &QCheckBox::toggled, this, &GraphicsPage::slotShadowDistLimitToggled); } bool Launcher::GraphicsPage::setupSDL() @@ -126,58 +125,6 @@ bool Launcher::GraphicsPage::loadSettings() framerateLimitSpinBox->setValue(fpsLimit); } - // Lighting - int lightingMethod = 1; - switch (Settings::shaders().mLightingMethod) - { - case SceneUtil::LightingMethod::FFP: - lightingMethod = 0; - break; - case SceneUtil::LightingMethod::PerObjectUniform: - lightingMethod = 1; - break; - case SceneUtil::LightingMethod::SingleUBO: - lightingMethod = 2; - break; - } - lightingMethodComboBox->setCurrentIndex(lightingMethod); - - // Shadows - if (Settings::shadows().mActorShadows) - actorShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::shadows().mPlayerShadows) - playerShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::shadows().mTerrainShadows) - terrainShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::shadows().mObjectShadows) - objectShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::shadows().mEnableIndoorShadows) - indoorShadowsCheckBox->setCheckState(Qt::Checked); - - const auto& boundMethod = Settings::shadows().mComputeSceneBounds.get(); - if (boundMethod == "bounds") - shadowComputeSceneBoundsComboBox->setCurrentIndex(0); - else if (boundMethod == "primitives") - shadowComputeSceneBoundsComboBox->setCurrentIndex(1); - else - shadowComputeSceneBoundsComboBox->setCurrentIndex(2); - - const int shadowDistLimit = Settings::shadows().mMaximumShadowMapDistance; - if (shadowDistLimit > 0) - { - shadowDistanceCheckBox->setCheckState(Qt::Checked); - shadowDistanceSpinBox->setValue(shadowDistLimit); - } - - const float shadowFadeStart = Settings::shadows().mShadowFadeStart; - if (shadowFadeStart != 0) - fadeStartSpinBox->setValue(shadowFadeStart); - - const int shadowRes = Settings::shadows().mShadowMapResolution; - int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes)); - if (shadowResIndex != -1) - shadowResolutionComboBox->setCurrentIndex(shadowResIndex); - return true; } @@ -220,53 +167,6 @@ void Launcher::GraphicsPage::saveSettings() { Settings::video().mFramerateLimit.set(0); } - - // Lighting - static constexpr std::array lightingMethodMap = { - SceneUtil::LightingMethod::FFP, - SceneUtil::LightingMethod::PerObjectUniform, - SceneUtil::LightingMethod::SingleUBO, - }; - Settings::shaders().mLightingMethod.set(lightingMethodMap[lightingMethodComboBox->currentIndex()]); - - // Shadows - const int cShadowDist = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0; - Settings::shadows().mMaximumShadowMapDistance.set(cShadowDist); - const float cFadeStart = fadeStartSpinBox->value(); - if (cShadowDist > 0) - Settings::shadows().mShadowFadeStart.set(cFadeStart); - - const bool cActorShadows = actorShadowsCheckBox->checkState() != Qt::Unchecked; - const bool cObjectShadows = objectShadowsCheckBox->checkState() != Qt::Unchecked; - const bool cTerrainShadows = terrainShadowsCheckBox->checkState() != Qt::Unchecked; - const bool cPlayerShadows = playerShadowsCheckBox->checkState() != Qt::Unchecked; - if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows) - { - Settings::shadows().mEnableShadows.set(true); - Settings::shadows().mActorShadows.set(cActorShadows); - Settings::shadows().mPlayerShadows.set(cPlayerShadows); - Settings::shadows().mObjectShadows.set(cObjectShadows); - Settings::shadows().mTerrainShadows.set(cTerrainShadows); - } - else - { - Settings::shadows().mEnableShadows.set(false); - Settings::shadows().mActorShadows.set(false); - Settings::shadows().mPlayerShadows.set(false); - Settings::shadows().mObjectShadows.set(false); - Settings::shadows().mTerrainShadows.set(false); - } - - Settings::shadows().mEnableIndoorShadows.set(indoorShadowsCheckBox->checkState() != Qt::Unchecked); - Settings::shadows().mShadowMapResolution.set(shadowResolutionComboBox->currentText().toInt()); - - auto index = shadowComputeSceneBoundsComboBox->currentIndex(); - if (index == 0) - Settings::shadows().mComputeSceneBounds.set("bounds"); - else if (index == 1) - Settings::shadows().mComputeSceneBounds.set("primitives"); - else - Settings::shadows().mComputeSceneBounds.set("none"); } QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) @@ -377,9 +277,3 @@ void Launcher::GraphicsPage::slotFramerateLimitToggled(bool checked) { framerateLimitSpinBox->setEnabled(checked); } - -void Launcher::GraphicsPage::slotShadowDistLimitToggled(bool checked) -{ - shadowDistanceSpinBox->setEnabled(checked); - fadeStartSpinBox->setEnabled(checked); -} diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp index 85f91d1ff1..928ec9f1a2 100644 --- a/apps/launcher/graphicspage.hpp +++ b/apps/launcher/graphicspage.hpp @@ -31,7 +31,6 @@ namespace Launcher void slotFullScreenChanged(int state); void slotStandardToggled(bool checked); void slotFramerateLimitToggled(bool checked); - void slotShadowDistLimitToggled(bool checked); private: QVector mResolutionsPerScreen; diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index 4aac90fb6e..78323458ce 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -41,11 +41,6 @@ int runLauncher(int argc, char* argv[]) appTranslator.load(":/translations/" + locale + ".qm"); app.installTranslator(&appTranslator); - // Now we make sure the current dir is set to application path - QDir dir(QCoreApplication::applicationDirPath()); - - QDir::setCurrent(dir.absolutePath()); - Launcher::MainDialog mainWin(configurationManager); Launcher::FirstRunDialogResult result = mainWin.showFirstRunDialog(); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index b8539671b5..c274b75f79 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -212,6 +212,65 @@ bool Launcher::SettingsPage::loadSettings() loadSettingBool(Settings::fog().mExponentialFog, *exponentialFogCheckBox); loadSettingBool(Settings::fog().mSkyBlending, *skyBlendingCheckBox); skyBlendingStartComboBox->setValue(Settings::fog().mSkyBlendingStart); + + loadSettingBool(Settings::shadows().mActorShadows, *actorShadowsCheckBox); + loadSettingBool(Settings::shadows().mPlayerShadows, *playerShadowsCheckBox); + loadSettingBool(Settings::shadows().mTerrainShadows, *terrainShadowsCheckBox); + loadSettingBool(Settings::shadows().mObjectShadows, *objectShadowsCheckBox); + loadSettingBool(Settings::shadows().mEnableIndoorShadows, *indoorShadowsCheckBox); + + const auto& boundMethod = Settings::shadows().mComputeSceneBounds.get(); + if (boundMethod == "bounds") + shadowComputeSceneBoundsComboBox->setCurrentIndex(0); + else if (boundMethod == "primitives") + shadowComputeSceneBoundsComboBox->setCurrentIndex(1); + else + shadowComputeSceneBoundsComboBox->setCurrentIndex(2); + + const int shadowDistLimit = Settings::shadows().mMaximumShadowMapDistance; + if (shadowDistLimit > 0) + { + shadowDistanceCheckBox->setCheckState(Qt::Checked); + shadowDistanceSpinBox->setValue(shadowDistLimit); + shadowDistanceSpinBox->setEnabled(true); + fadeStartSpinBox->setEnabled(true); + } + + const float shadowFadeStart = Settings::shadows().mShadowFadeStart; + if (shadowFadeStart != 0) + fadeStartSpinBox->setValue(shadowFadeStart); + + const int shadowRes = Settings::shadows().mShadowMapResolution; + int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes)); + if (shadowResIndex != -1) + shadowResolutionComboBox->setCurrentIndex(shadowResIndex); + + connect(shadowDistanceCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotShadowDistLimitToggled); + + lightsMaxLightsSpinBox->setValue(Settings::shaders().mMaxLights); + lightsMaximumDistanceSpinBox->setValue(Settings::shaders().mMaximumLightDistance); + lightFadeMultiplierSpinBox->setValue(Settings::shaders().mLightFadeStart); + lightsBoundingSphereMultiplierSpinBox->setValue(Settings::shaders().mLightBoundsMultiplier); + lightsMinimumInteriorBrightnessSpinBox->setValue(Settings::shaders().mMinimumInteriorBrightness); + + connect(lightingMethodComboBox, qOverload(&QComboBox::currentIndexChanged), this, + &SettingsPage::slotLightTypeCurrentIndexChanged); + + int lightingMethod = 1; + switch (Settings::shaders().mLightingMethod) + { + case SceneUtil::LightingMethod::FFP: + lightingMethod = 0; + break; + case SceneUtil::LightingMethod::PerObjectUniform: + lightingMethod = 1; + break; + case SceneUtil::LightingMethod::SingleUBO: + lightingMethod = 2; + break; + } + lightingMethodComboBox->setCurrentIndex(lightingMethod); + slotLightTypeCurrentIndexChanged(lightingMethod); } // Audio @@ -359,6 +418,58 @@ void Launcher::SettingsPage::saveSettings() saveSettingBool(*exponentialFogCheckBox, Settings::fog().mExponentialFog); saveSettingBool(*skyBlendingCheckBox, Settings::fog().mSkyBlending); Settings::fog().mSkyBlendingStart.set(skyBlendingStartComboBox->value()); + + static constexpr std::array lightingMethodMap = { + SceneUtil::LightingMethod::FFP, + SceneUtil::LightingMethod::PerObjectUniform, + SceneUtil::LightingMethod::SingleUBO, + }; + Settings::shaders().mLightingMethod.set(lightingMethodMap[lightingMethodComboBox->currentIndex()]); + + const int cShadowDist + = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0; + Settings::shadows().mMaximumShadowMapDistance.set(cShadowDist); + const float cFadeStart = fadeStartSpinBox->value(); + if (cShadowDist > 0) + Settings::shadows().mShadowFadeStart.set(cFadeStart); + + const bool cActorShadows = actorShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cObjectShadows = objectShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cTerrainShadows = terrainShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cPlayerShadows = playerShadowsCheckBox->checkState() != Qt::Unchecked; + if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows) + { + Settings::shadows().mEnableShadows.set(true); + Settings::shadows().mActorShadows.set(cActorShadows); + Settings::shadows().mPlayerShadows.set(cPlayerShadows); + Settings::shadows().mObjectShadows.set(cObjectShadows); + Settings::shadows().mTerrainShadows.set(cTerrainShadows); + } + else + { + Settings::shadows().mEnableShadows.set(false); + Settings::shadows().mActorShadows.set(false); + Settings::shadows().mPlayerShadows.set(false); + Settings::shadows().mObjectShadows.set(false); + Settings::shadows().mTerrainShadows.set(false); + } + + Settings::shadows().mEnableIndoorShadows.set(indoorShadowsCheckBox->checkState() != Qt::Unchecked); + Settings::shadows().mShadowMapResolution.set(shadowResolutionComboBox->currentText().toInt()); + + auto index = shadowComputeSceneBoundsComboBox->currentIndex(); + if (index == 0) + Settings::shadows().mComputeSceneBounds.set("bounds"); + else if (index == 1) + Settings::shadows().mComputeSceneBounds.set("primitives"); + else + Settings::shadows().mComputeSceneBounds.set("none"); + + Settings::shaders().mMaxLights.set(lightsMaxLightsSpinBox->value()); + Settings::shaders().mMaximumLightDistance.set(lightsMaximumDistanceSpinBox->value()); + Settings::shaders().mLightFadeStart.set(lightFadeMultiplierSpinBox->value()); + Settings::shaders().mLightBoundsMultiplier.set(lightsBoundingSphereMultiplierSpinBox->value()); + Settings::shaders().mMinimumInteriorBrightness.set(lightsMinimumInteriorBrightnessSpinBox->value()); } // Audio @@ -461,3 +572,17 @@ void Launcher::SettingsPage::slotSkyBlendingToggled(bool checked) skyBlendingStartComboBox->setEnabled(checked); skyBlendingStartLabel->setEnabled(checked); } + +void Launcher::SettingsPage::slotShadowDistLimitToggled(bool checked) +{ + shadowDistanceSpinBox->setEnabled(checked); + fadeStartSpinBox->setEnabled(checked); +} + +void Launcher::SettingsPage::slotLightTypeCurrentIndexChanged(int index) +{ + lightsMaximumDistanceSpinBox->setEnabled(index != 0); + lightsMaxLightsSpinBox->setEnabled(index != 0); + lightsBoundingSphereMultiplierSpinBox->setEnabled(index != 0); + lightsMinimumInteriorBrightnessSpinBox->setEnabled(index != 0); +} diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index 9f7d6b1f43..ea675857ea 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -32,6 +32,8 @@ namespace Launcher void slotAnimSourcesToggled(bool checked); void slotPostProcessToggled(bool checked); void slotSkyBlendingToggled(bool checked); + void slotShadowDistLimitToggled(bool checked); + void slotLightTypeCurrentIndexChanged(int index); private: Config::GameSettings& mGameSettings; diff --git a/apps/launcher/ui/graphicspage.ui b/apps/launcher/ui/graphicspage.ui index c0e2b0be06..fa92c7b789 100644 --- a/apps/launcher/ui/graphicspage.ui +++ b/apps/launcher/ui/graphicspage.ui @@ -11,459 +11,229 @@ - - - - 0 - - - - Display - - - - - - + + + + + + + + + Screen + + + + + + + Window mode + + + + + + + + + + + 800 + + + + + + + × + + + + + + + 600 + + + + + + + + + Custom: + + + + + + + Standard: + + + true + + + + + + + + + + + - Screen: + 0 - - - - - - - 0 - - - - - 2 - - - - - 4 - - - - - 8 - - - - - 16 - - - - - - - - - + + - Window Mode: + 2 - - - - + + - Resolution: + 4 - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - + + - Vertical Sync: + 8 - - - - + + - Anti-aliasing: + 16 - - - - - - - - - - 800 - - - - - - - × - - - - - - - 600 - - - - - - - - - Custom: - - - - - - - Standard: - - - true - - - - - - - - - - - - 0 - - - - Fullscreen - - - - - Windowed Fullscreen - - - - - Windowed - - - - - - + + + + + + + Framerate limit + + + + + + + Window border + + + + + + + + + + 0 + + - Window Border + Disabled - - - - - - 0 - - - - Disabled - - - - - Enabled - - - - - Adaptive - - - - - - + + - Framerate Limit: + Enabled - - - - - - false - - - FPS - - - 1 - - - 1.000000000000000 - - - 1000.000000000000000 - - - 15.000000000000000 - - - 300.000000000000000 - - - - - - - - - - Lighting - - - - - - + + - Lighting Method: - - - - - - - - legacy - - - - - shaders compatibility - - - - - shaders - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Shadows - - - - - - - - <html><head/><body><p>Enable shadows exclusively for the player character. May have a very minor performance impact.</p></body></html> + Adaptive + + + + + + + 0 + + - Enable Player Shadows - - - - - - - <html><head/><body><p>Enable shadows for NPCs and creatures besides the player character. May have a minor performance impact.</p></body></html> + Fullscreen + + - Enable Actor Shadows - - - - - - - <html><head/><body><p>Enable shadows for primarily inanimate objects. May have a significant performance impact.</p></body></html> + Windowed Fullscreen + + - Enable Object Shadows + Windowed - - - - - - <html><head/><body><p>Enable shadows for the terrain including distant terrain. May have a significant performance and shadow quality impact.</p></body></html> - - - Enable Terrain Shadows - - - - - - - <html><head/><body><p>Due to limitations with Morrowind's data, only actors can cast shadows indoors, which some might feel is distracting.</p><p>Has no effect if actor/player shadows are not enabled.</p></body></html> - - - Enable Indoor Shadows - - - - - - - <html><head/><body><p>Type of "compute scene bounds" computation method to be used. Bounds (default) for good balance between performance and shadow quality, primitives for better looking shadows or none for no computation.</p></body></html> - - - Shadow Near Far Computation Method: - - - - - - - - bounds - - - - - primitives - - - - - none - - - - - - - - <html><head/><body><p>The resolution of each individual shadow map. Increasing it significantly improves shadow quality but may have a minor performance impact.</p></body></html> - - - Shadow Map Resolution: - - - - - - - - 512 - - - - - 1024 - - - - - 2048 - - - - - 4096 - - - - - - - - <html><head/><body><p>The distance from the camera at which shadows completely disappear.</p></body></html> - - - Shadow Distance Limit: - - - - - - - false - - - <html><head/><body><p>64 game units is 1 real life yard or about 0.9 m</p></body></html> - - - unit(s) - - - 512 - - - 81920 - - - 8192 - - - - - - - <html><head/><body><p>The fraction of the limit above at which shadows begin to gradually fade away.</p></body></html> - - - Fade Start Multiplier: - - - - - - - false - - - 2 - - - 0.000000000000000 - - - 1.000000000000000 - - - 0.900000000000000 - - - - - - - + + + + + + + Resolution + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + false + + + FPS + + + 1 + + + 1.000000000000000 + + + 1000.000000000000000 + + + 15.000000000000000 + + + 300.000000000000000 + + + + + + + Anti-aliasing + + + + + + + Vertical synchronization + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + diff --git a/apps/launcher/ui/importpage.ui b/apps/launcher/ui/importpage.ui index 3e2b0c5e64..4626d29e8a 100644 --- a/apps/launcher/ui/importpage.ui +++ b/apps/launcher/ui/importpage.ui @@ -6,7 +6,7 @@ 0 0 - 514 + 515 397 @@ -129,16 +129,22 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov + + + + Qt::Vertical + + + + 0 + 0 + + + + - - - - Qt::Vertical - - - diff --git a/apps/launcher/ui/mainwindow.ui b/apps/launcher/ui/mainwindow.ui index 54a369999d..b05a9d2c3b 100644 --- a/apps/launcher/ui/mainwindow.ui +++ b/apps/launcher/ui/mainwindow.ui @@ -7,20 +7,20 @@ 0 0 720 - 565 + 635 720 - 565 + 635 OpenMW Launcher - + :/images/openmw.png:/images/openmw.png @@ -120,7 +120,7 @@ QToolButton { true - + :/images/openmw-plugin.png:/images/openmw-plugin.png @@ -141,11 +141,11 @@ QToolButton { true - + :/images/preferences-video.png:/images/preferences-video.png - Graphics + Display Allows to change graphics settings @@ -156,7 +156,7 @@ QToolButton { true - + :/images/preferences.png:/images/preferences.png @@ -171,7 +171,7 @@ QToolButton { true - + :/images/preferences-advanced.png:/images/preferences-advanced.png @@ -183,7 +183,7 @@ QToolButton { - + diff --git a/apps/launcher/ui/settingspage.ui b/apps/launcher/ui/settingspage.ui index 0340509205..7006238e71 100644 --- a/apps/launcher/ui/settingspage.ui +++ b/apps/launcher/ui/settingspage.ui @@ -6,7 +6,7 @@ 0 0 - 732 + 741 503 @@ -14,36 +14,16 @@ - 1 + 0 - + - Game Mechanics + Gameplay - - - - <html><head/><body><p>Make disposition change of merchants caused by trading permanent.</p></body></html> - - - Permanent barter disposition changes - - - - - - - <html><head/><body><p>Make player followers and escorters start combat with enemies who have started combat with them or the player. Otherwise they wait for the enemies or the player to do an attack first.</p></body></html> - - - Followers defend immediately - - - - + <html><head/><body><p>Make Damage Fatigue magic effect uncapped like Drain Fatigue effect.</p><p>This means that unlike Morrowind you will be able to knock down actors using this effect.</p></body></html> @@ -53,7 +33,37 @@ - + + + + Give actors an ability to swim over the water surface when they follow other actor independently from their ability to swim. Has effect only when nav mesh building is enabled. + + + Always allow actors to follow over water + + + + + + + <html><head/><body><p>Make disposition change of merchants caused by trading permanent.</p></body></html> + + + Permanent barter disposition changes + + + + + + + <html><head/><body><p>Don't use race weight in NPC movement speed calculations.</p></body></html> + + + Racial variation in speed fix + + + + <html><head/><body><p>Stops combat with NPCs affected by Calm spells every frame -- like in Morrowind without the MCP.</p></body></html> @@ -63,7 +73,17 @@ - + + + + <html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html> + + + NPCs avoid collisions + + + + <html><head/><body><p>Make the value of filled soul gems dependent only on soul magnitude.</p></body></html> @@ -73,47 +93,27 @@ - - + + - <html><head/><body><p>Makes player swim a bit upward from the line of sight. Applies only in third person mode. Intended to make simpler swimming without diving.</p></body></html> + <html><head/><body><p>If this setting is true, supporting models will make use of day night switch nodes.</p></body></html> - Swim upward correction + Day night switch nodes - - + + - <html><head/><body><p>Make enchanted weaponry without Magical flag bypass normal weapons resistance, like in Morrowind.</p></body></html> + <html><head/><body><p>Make player followers and escorters start combat with enemies who have started combat with them or the player. Otherwise they wait for the enemies or the player to do an attack first.</p></body></html> - Enchanted weapons are magical + Followers defend immediately - - - - <html><head/><body><p>Make stealing items from NPCs that were knocked down possible during combat.</p></body></html> - - - Always allow stealing from knocked out actors - - - - - - - <html><head/><body><p>Effects of reflected Absorb spells are not mirrored -- like in Morrowind.</p></body></html> - - - Classic reflected Absorb spells behavior - - - - + <html><head/><body><p><a name="docs-internal-guid-f375b85a-7fff-02ff-a5af-c5cff63923c0"/>When enabled, a navigation mesh is built in the background for world geometry to be used for pathfinding. When disabled only the path grid is used to build paths. Single-core CPU systems may have a big performance impact on existing interior location and moving across the exterior world. May slightly affect performance on multi-core CPU systems. Multi-core CPU systems may have different latency for nav mesh update depending on other settings and system performance. Moving across external world, entering/exiting location produce nav mesh update. NPC and creatures may not be able to find path before nav mesh is built around them. Try to disable this if you want to have old fashioned AI which doesn't know where to go when you stand behind that stone and cast a firebolt.</p></body></html> @@ -124,26 +124,66 @@ - + - <html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html> + <html><head/><body><p>If enabled, a magical ammunition is required to bypass normal weapon resistance or weakness. If disabled, a magical ranged weapon or a magical ammunition is required.</p></body></html> - NPCs avoid collisions + Only magical ammo bypass resistance - - + + - <html><head/><body><p>Don't use race weight in NPC movement speed calculations.</p></body></html> + <html><head/><body><p>If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html> - Racial variation in speed fix + Graphic herbalism - + + + + <html><head/><body><p>Makes player swim a bit upward from the line of sight. Applies only in third person mode. Intended to make simpler swimming without diving.</p></body></html> + + + Swim upward correction + + + + + + + <html><head/><body><p>Make enchanted weapons without Magical flag bypass normal weapons resistance, like in Morrowind.</p></body></html> + + + Enchanted weapons are magical + + + + + + + <html><head/><body><p>Prevents merchants from equipping items that are sold to them.</p></body></html> + + + Merchant equipping fix + + + + + + + <html><head/><body><p>Trainers now only choose which skills to train using their base skill points, allowing mercantile improving effects to be used without making mercantile an offered skill.</p></body></html> + + + Trainers choose offered skills by base value + + + + <html><head/><body><p>If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.</p><p>If this setting is false, player has to wait until end of death animation in all cases. 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.</p></body></html> @@ -153,44 +193,44 @@ - - + + - Give NPC an ability to swim over the water surface when they follow other actor independently from their ability to swim. Has effect only when nav mesh building is enabled. + <html><head/><body><p>Make stealing items from NPCs that were knocked down possible during combat.</p></body></html> - Always allow NPC to follow over water surface + Steal from knocked out actors in combat + + + + + + + <html><head/><body><p>Effects of reflected Absorb spells are not mirrored -- like in Morrowind.</p></body></html> + + + Classic reflected Absorb spells behavior + + + + + + + <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> + + + Unarmed creature attacks damage armor - - - - <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> - - - Unarmed creature attacks damage armor - - - - - - - <html><head/><body><p>Allow non-standard ammunition solely to bypass normal weapon resistance or weakness.</p></body></html> - - - Only appropriate ammunition bypasses normal weapon resistance - - - - Factor strength into hand-to-hand combat: + Factor strength into hand-to-hand combat @@ -222,7 +262,7 @@ <html><head/><body><p>How many threads will be spawned to compute physics update in the background. A value of 0 means that the update will be performed in the main thread.</p><p>A value greater than 1 requires the Bullet library be compiled with multithreading support.</p></body></html> - Background physics threads: + Background physics threads @@ -232,14 +272,14 @@ - Actor collision shape type: + Actor collision shape type - Collision is used for both physics simulation and navigation mesh generation for pathfinding. Cylinder gives the best consistency bewtween available navigation paths and ability to move by them. Changing this value affects navigation mesh generation therefore navigation mesh disk cache generated for one value will not be useful with another. + Collision is used for both physics simulation and navigation mesh generation for pathfinding. Cylinder gives the best consistency between available navigation paths and ability to move by them. Changing this value affects navigation mesh generation therefore navigation mesh disk cache generated for one value will not be useful with another. Axis-aligned bounding box @@ -284,492 +324,870 @@ - - - true - - - - - 0 - 0 - 671 - 774 - - - - - - - Animations - - - - - - <html><head/><body><p>Use casting animations for magic items, just as for spells.</p></body></html> - - - Use magic item animation - - - - - - - <html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html> - - - Smooth movement - - - - - - - <html><head/><body><p>Load per-group KF-files and skeleton files from Animations folder</p></body></html> - - - Use additional animation sources - - - - - - - <html><head/><body><p>Affects side and diagonal movement. Enabling this setting makes movement more realistic.</p><p>If disabled then the whole character's body is pointed to the direction of view. Diagonal movement has no special animation and causes sliding.</p><p>If enabled then the character turns lower body to the direction of movement. Upper body is turned partially. Head is always pointed to the direction of view. In combat mode it works only for diagonal movement. In non-combat mode it changes straight right and straight left movement as well. Also turns the whole body up or down when swimming according to the movement direction.</p></body></html> - - - Turn to movement direction - - - - - - - 20 - - - - - false - - - <html><head/><body><p>Render holstered weapons (with quivers and scabbards), requires modded assets.</p></body></html> - - - Weapon sheathing - - - - - - - false - - - <html><head/><body><p>Render holstered shield, requires modded assets.</p></body></html> - - - Shield sheathing - - - - - - - - - <html><head/><body><p>In third person, the camera will sway along with the movement animations of the player. Enabling this option disables this swaying by having the player character move independently of its animation. This was the default behavior of OpenMW 0.48 and earlier.</p></body></html> - - - Player movement ignores animation - - - - - - - - - - Shaders - - - - - - <html><head/><body><p>If this option is enabled, normal maps are automatically recognized and used if they are named appropriately + + + + + 0 + + + + Animations + + + + + + + + <html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html> + + + Smooth movement + + + + + + + <html><head/><body><p>Load per-group KF-files and skeleton files from Animations folder</p></body></html> + + + Use additional animation sources + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Affects side and diagonal movement. Enabling this setting makes movement more realistic.</p><p>If disabled then the whole character's body is pointed to the direction of view. Diagonal movement has no special animation and causes sliding.</p><p>If enabled then the character turns lower body to the direction of movement. Upper body is turned partially. Head is always pointed to the direction of view. In combat mode it works only for diagonal movement. In non-combat mode it changes straight right and straight left movement as well. Also turns the whole body up or down when swimming according to the movement direction.</p></body></html> + + + Turn to movement direction + + + + + + + false + + + <html><head/><body><p>Render holstered weapons (with quivers and scabbards), requires modded assets.</p></body></html> + + + Weapon sheathing + + + + + + + false + + + <html><head/><body><p>Render holstered shield, requires modded assets.</p></body></html> + + + Shield sheathing + + + + + + + <html><head/><body><p>In third person, the camera will sway along with the movement animations of the player. Enabling this option disables this swaying by having the player character move independently of its animation. This was the default behavior of OpenMW 0.48 and earlier.</p></body></html> + + + Player movement ignores animation + + + + + + + <html><head/><body><p>Use casting animations for magic items, just as for spells.</p></body></html> + + + Use magic item animation + + + + + + + + + + Shaders + + + + + + + + + + <html><head/><body><p>If this option is enabled, normal maps are automatically recognized and used if they are named appropriately (see 'normal map pattern', e.g. for a base texture foo.dds, the normal map texture would have to be named foo_n.dds). If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file (.nif or .osg file). Affects objects.</p></body></html> - - - Auto use object normal maps - - - - - - - <html><head/><body><p>See 'auto use object normal maps'. Affects terrain.</p></body></html> - - - Auto use terrain normal maps - - - - - - - <html><head/><body><p>If this option is enabled, specular maps are automatically recognized and used if they are named appropriately + + + Auto use object normal maps + + + + + + + <html><head/><body><p>Enables soft particles for particle effects. This technique softens the intersection between individual particles and other opaque geometry by blending between them.</p></body></html> + + + Soft particles + + + + + + + <html><head/><body><p>If this option is enabled, specular maps are automatically recognized and used if they are named appropriately (see 'specular map pattern', e.g. for a base texture foo.dds, the specular map texture would have to be named foo_spec.dds). If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file (.osg file, not supported in .nif files). Affects objects.</p></body></html> - - - Auto use object specular maps - - - - - - - <html><head/><body><p>If a file with pattern 'terrain specular map pattern' exists, use that file as a 'diffuse specular' map. The texture must contain the layer colour in the RGB channel (as usual), and a specular multiplier in the alpha channel.</p></body></html> - - - Auto use terrain specular maps - - - - - - - <html><head/><body><p>Normally environment map reflections aren't affected by lighting, which makes environment-mapped (and thus bump-mapped objects) glow in the dark. + + + Auto use object specular maps + + + + + + + <html><head/><body><p>See 'auto use object normal maps'. Affects terrain.</p></body></html> + + + Auto use terrain normal maps + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>If a file with pattern 'terrain specular map pattern' exists, use that file as a 'diffuse specular' map. The texture must contain the layer colour in the RGB channel (as usual), and a specular multiplier in the alpha channel.</p></body></html> + + + Auto use terrain specular maps + + + + + + + <html><head/><body><p>Simulate coverage-preserving mipmaps to prevent alpha-tested meshes shrinking as they get further away. Will cause meshes whose textures have coverage-preserving mipmaps to grow, though, so refer to mod installation instructions for how to set this.</p></body></html> + + + Adjust coverage for alpha test + + + + + + + <html><head/><body><p>Allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation. Can negatively impact performance.</p></body></html> + + + Use anti-alias alpha testing + + + + + + + <html><head/><body><p>Normally environment map reflections aren't affected by lighting, which makes environment-mapped (and thus bump-mapped objects) glow in the dark. Morrowind Code Patch includes an option to remedy that by doing environment-mapping before applying lighting, this is the equivalent of that option. Affected objects will use shaders. </p></body></html> - - - Bump/reflect map local lighting - - - - - - - <html><head/><body><p>Allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation. Can negatively impact performance.</p></body></html> - - - Use anti-alias alpha testing - - - - - - - <html><head/><body><p>Enables soft particles for particle effects. This technique softens the intersection between individual particles and other opaque geometry by blending between them.</p></body></html> - - - Soft Particles - - - - - - - <html><head/><body><p>Simulate coverage-preserving mipmaps to prevent alpha-tested meshes shrinking as they get further away. Will cause meshes whose textures have coverage-preserving mipmaps to grow, though, so refer to mod installation instructions for how to set this.</p></body></html> - - - Adjust coverage for alpha test - - - - - - - <html><head/><body><p>EXPERIMENTAL: Stop rain and snow from falling through overhangs and roofs.</p></body></html> - - - Weather Particle Occlusion - - - - - - - - - - Fog - - - - - - <html><head/><body><p>By default, the fog becomes thicker proportionally to your distance from the clipping plane set at the clipping distance, which causes distortion at the edges of the screen. + + + Bump/reflect map local lighting + + + + + + + <html><head/><body><p>EXPERIMENTAL: Stop rain and snow from falling through overhangs and roofs.</p></body></html> + + + Weather particle occlusion + + + + + + + + + + + + Fog + + + + + + + + <html><head/><body><p>Use exponential fog formula. By default, linear fog is used.</p></body></html> + + + Exponential fog + + + + + + + false + + + 3 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.005000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>By default, the fog becomes thicker proportionally to your distance from the clipping plane set at the clipping distance, which causes distortion at the edges of the screen. This setting makes the fog use the actual eye point distance (or so called Euclidean distance) to calculate the fog, which makes the fog look less artificial, especially if you have a wide FOV.</p></body></html> - - - Radial fog - - - - - - - <html><head/><body><p>Use exponential fog formula. By default, linear fog is used.</p></body></html> - - - Exponential fog - - - - - - - <html><head/><body><p>Reduce visibility of clipping plane by blending objects with the sky.</p></body></html> - - - Sky blending - - - - - - - false - - - <html><head/><body><p>The fraction of the maximum distance at which blending with the sky starts.</p></body></html> - - - Sky blending start - - - - - - - false - - - 3 - - - 0.000000000000000 - - - 1.000000000000000 - - - 0.005000000000000 - - - - - - - - - - Terrain - - - - - - + + + Radial fog + + + + + + + false + + + <html><head/><body><p>The fraction of the maximum distance at which blending with the sky starts.</p></body></html> + + + Sky blending start + + + + + + + <html><head/><body><p>Reduce visibility of clipping plane by blending objects with the sky.</p></body></html> + + + Sky blending + + + + + + + + + + Terrain + + + + + + + + <html><head/><body><p>If true, use paging and LOD algorithms to display the entire terrain. If false, only display terrain of the loaded cells.</p></body></html> + + + Distant land + + + + + + + cells + + + 0.000000000000000 + + + 0.500000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Controls how large an object must be to be visible in the scene. The object’s size is divided by its distance to the camera and the result of the division is compared with this value. The smaller this value is, the more objects you will see in the scene.</p></body></html> + + + Object paging min size + + + + + + + Viewing distance + + + + + + + 3 + + + 0.000000000000000 + + + 0.250000000000000 + + + 0.005000000000000 + + + + + + + <html><head/><body><p>Use object paging for active cells grid.</p></body></html> + + + Active grid object paging + + + + + + + + + + Post Processing + + + + + + + + false + + + <html><head/><body><p>Controls how much eye adaptation can change from frame to frame. Smaller values makes for slower transitions.</p></body></html> + + + Auto exposure speed + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + false + + + <html><head/><body><p>Re-render transparent objects with forced alpha clipping.</p></body></html> + + + Transparent postpass + + + + + + + false + + + 3 + + + 0.010000000000000 + + + 10.000000000000000 + + + 0.001000000000000 + + + + + + + <html><head/><body><p>If this setting is true, post processing will be enabled.</p></body></html> + + + Enable post processing + + + + + + + + + + Shadows + + + + + + + - Viewing distance - - - - - - - Cells - - - 0.000000000000000 - - - 0.500000000000000 - - - - - - - - - - - <html><head/><body><p>Controls how large an object must be to be visible in the scene. The object’s size is divided by its distance to the camera and the result of the division is compared with this value. The smaller this value is, the more objects you will see in the scene.</p></body></html> + bounds + + - Object paging min size - - - - - - - 3 - - - 0.000000000000000 - - - 0.250000000000000 - - - 0.005000000000000 - - - - - - - - - <html><head/><body><p>If true, use paging and LOD algorithms to display the entire terrain. If false, only display terrain of the loaded cells.</p></body></html> - - - Distant land - - - - - - - 20 - - - - - <html><head/><body><p>Use object paging for active cells grid.</p></body></html> + primitives + + - Active grid object paging - - - - - - - - - - - - Models - - - - - - <html><head/><body><p>If this setting is true, supporting models will make use of day night switch nodes.</p></body></html> - - - Day night switch nodes - - - - - - - - - - Post Processing - - - - - - <html><head/><body><p>If this setting is true, post processing will be enabled.</p></body></html> - - - Enable post processing - - - - - - - 20 - - - - - false - - - <html><head/><body><p>Re-render transparent objects with forced alpha clipping.</p></body></html> + none + + + + + + + <html><head/><body><p>Type of "compute scene bounds" computation method to be used. Bounds (default) for good balance between performance and shadow quality, primitives for better looking shadows or none for no computation.</p></body></html> + + + Shadow planes computation method + + + + + + + false + + + <html><head/><body><p>64 game units is 1 real life yard or about 0.9 m</p></body></html> + + + unit(s) + + + 512 + + + 81920 + + + 8192 + + + + + + + <html><head/><body><p>Enable shadows for NPCs and creatures besides the player character. May have a minor performance impact.</p></body></html> + + + Enable actor shadows + + + + + + - Transparent postpass + 512 - - - - - - - - false - - - <html><head/><body><p>Controls how much eye adaptation can change from frame to frame. Smaller values makes for slower transitions.</p></body></html> - - - Auto exposure speed - - - - - - - false - - - 3 - - - 0.010000000000000 - - - 10.000000000000000 - - - 0.001000000000000 - - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - + + + + 1024 + + + + + 2048 + + + + + 4096 + + + + + + + + <html><head/><body><p>The fraction of the limit above at which shadows begin to gradually fade away.</p></body></html> + + + Fade start multiplier + + + + + + + <html><head/><body><p>Enable shadows exclusively for the player character. May have a very minor performance impact.</p></body></html> + + + Enable player shadows + + + + + + + <html><head/><body><p>The resolution of each individual shadow map. Increasing it significantly improves shadow quality but may have a minor performance impact.</p></body></html> + + + Shadow map resolution + + + + + + + <html><head/><body><p>The distance from the camera at which shadows completely disappear.</p></body></html> + + + Shadow distance limit: + + + + + + + <html><head/><body><p>Enable shadows for primarily inanimate objects. May have a significant performance impact.</p></body></html> + + + Enable object shadows + + + + + + + false + + + 2 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.900000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Due to limitations with Morrowind's data, only actors can cast shadows indoors, which some might feel is distracting.</p><p>Has no effect if actor/player shadows are not enabled.</p></body></html> + + + Enable indoor shadows + + + + + + + <html><head/><body><p>Enable shadows for the terrain including distant terrain. May have a significant performance and shadow quality impact.</p></body></html> + + + Enable terrain shadows + + + + + + + + + + Lighting + + + + + + + + <html><head/><body><p>Maximum distance at which lights will appear (measured in units).</p><p>Set this to 0 to use an unlimited distance.</p></body></html> + + + Lights maximum distance + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Maximum number of lights per object.</p><p>A low number near default will cause light popping similar to what you would see with legacy lighting.</p></body></html> + + + Max light sources + + + + + + + <html><head/><body><p>Fraction of maximum distance at which lights will start to fade.</p><p>Set this to a low value for slower transitions or a high value for quicker transitions.</p></body></html> + + + Lights fade multiplier + + + + + + + <html><head/><body><p>Set the internal handling of light sources.</p> +<p> "Legacy" always uses 8 lights per object and provides a lighting closest to an original game.</p> +<p>"Shaders (compatibility)" removes the 8 light limit. This mode also enables lighting on groundcover and a configurable light fade. It is recommended to use this with older hardware and a light limit closer to 8.</p> +<p> "Shaders" carries all of the benefits that "Shaders (compatibility)" does, but uses a modern approach that allows for a higher max lights count with little to no performance penalties on modern hardware.</p></body></html> + + + Lighting method + + + + + + + + Legacy + + + + + Shaders (compatibility) + + + + + Shaders + + + + + + + + <html><head/><body><p>Multipler for bounding sphere of lights.</p><p>Higher numbers allows for smooth falloff but require an increase in number of max lights.</p><p>Does not effect the illumination or strength of lights.</p></body></html> + + + Lights bounding sphere multiplier + + + + + + + <html><head/><body><p>Minimum ambient interior brightness.</p><p>Increase this if you feel interiors are too dark.</p></body></html> + + + Lights minimum interior brightness + + + + + + + 5.000000000000000 + + + 0.050000000000000 + + + 1.650000000000000 + + + + + + + 1.000000000000000 + + + 0.010000000000000 + + + 0.850000000000000 + + + + + + + 1.000000000000000 + + + 0.010000000000000 + + + 0.080000000000000 + + + + + + + unit(s) + + + 8192 + + + 128 + + + 8192 + + + + + + + 2 + + + 64 + + + 2 + + + + + + + + + + @@ -786,7 +1204,7 @@ Select your preferred audio device. - Audio Device + Audio device @@ -872,7 +1290,7 @@ Select your preferred HRTF profile. - HRTF Profile + HRTF profile @@ -936,17 +1354,17 @@ - Tool Tip Only + Tooltip - Crosshair Only + Crosshair - Tool Tip and Crosshair + Tooltip and crosshair @@ -1036,16 +1454,6 @@ - - - - <html><head/><body><p>If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html> - - - Enable graphic herbalism - - - @@ -1110,46 +1518,6 @@ - - - Bug Fixes - - - - - - <html><head/><body><p>Prevents merchants from equipping items that are sold to them.</p></body></html> - - - Merchant equipping fix - - - - - - - <html><head/><body><p>Trainers now only choose which skills to train using their base skill points, allowing mercantile improving effects to be used without making mercantile an offered skill.</p></body></html> - - - Trainers choose their training skills based on their base skill points - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - Miscellaneous @@ -1176,7 +1544,7 @@ - Maximum Quicksaves + Maximum quicksaves @@ -1193,9 +1561,9 @@ - + - Other + Screenshots @@ -1203,7 +1571,7 @@ - Screenshot Format + Screenshot format diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index 8604bcdfb0..9ed7fb4c2e 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -221,7 +221,7 @@ namespace NavMeshTool constexpr double expiryDelay = 0; Resource::ImageManager imageManager(&vfs, expiryDelay); - Resource::NifFileManager nifFileManager(&vfs); + Resource::NifFileManager nifFileManager(&vfs, &encoder.getStatelessEncoder()); Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, expiryDelay); Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); DetourNavigator::RecastGlobalAllocator::init(); diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index 29488fb677..a0dbb1233b 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include @@ -33,7 +34,7 @@ bool hasExtension(const std::filesystem::path& filename, const std::string& exte /// See if the file has the "nif" extension. bool isNIF(const std::filesystem::path& filename) { - return hasExtension(filename, ".nif"); + return hasExtension(filename, ".nif") || hasExtension(filename, ".kf"); } /// See if the file has the "bsa" extension. bool isBSA(const std::filesystem::path& filename) @@ -75,7 +76,10 @@ void readNIF( const std::string pathStr = Files::pathToUnicodeString(path); if (!quiet) { - std::cout << "Reading NIF file '" << pathStr << "'"; + if (hasExtension(path, ".kf")) + std::cout << "Reading KF file '" << pathStr << "'"; + else + std::cout << "Reading NIF file '" << pathStr << "'"; if (!source.empty()) std::cout << " from '" << Files::pathToUnicodeString(isBSA(source) ? source.filename() : source) << "'"; std::cout << std::endl; @@ -84,7 +88,7 @@ void readNIF( try { Nif::NIFFile file(fullPath); - Nif::Reader reader(file); + Nif::Reader reader(file, nullptr); if (vfs != nullptr) reader.parse(vfs->get(pathStr)); else @@ -138,10 +142,10 @@ void readVFS(std::unique_ptr&& archive, const std::filesystem::pat bool parseOptions(int argc, char** argv, Files::PathContainer& files, Files::PathContainer& archives, bool& writeDebugLog, bool& quiet) { - bpo::options_description desc(R"(Ensure that OpenMW can use the provided NIF and BSA files + bpo::options_description desc(R"(Ensure that OpenMW can use the provided NIF, KF and BSA/BA2 files Usages: - niftest + niftest Scan the file or directories for NIF errors. Allowed options)"); @@ -240,7 +244,8 @@ int main(int argc, char** argv) } else { - std::cerr << "Error: '" << pathStr << "' is not a NIF file, BSA/BA2 archive, or directory" << std::endl; + std::cerr << "Error: '" << pathStr << "' is not a NIF/KF file, BSA/BA2 archive, or directory" + << std::endl; } } catch (std::exception& e) diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index ecab9614a1..e7f980dc0d 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -81,11 +81,6 @@ int runApplication(int argc, char* argv[]) Application application(argc, argv); -#ifdef Q_OS_MAC - QDir dir(QCoreApplication::applicationDirPath()); - QDir::setCurrent(dir.absolutePath()); -#endif - application.setWindowIcon(QIcon(":./openmw-cs.png")); CS::Editor editor(argc, argv); diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 5443db2854..aadad5f8f5 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -624,7 +624,7 @@ bool CSMFilter::Parser::parse(const std::string& filter, bool allowPredefined) } if (node) - mFilter = node; + mFilter = std::move(node); else { // Empty filter string equals to filter "true". diff --git a/apps/opencs/model/tools/reportmodel.cpp b/apps/opencs/model/tools/reportmodel.cpp index 84a8c71f95..f9251acdab 100644 --- a/apps/opencs/model/tools/reportmodel.cpp +++ b/apps/opencs/model/tools/reportmodel.cpp @@ -171,7 +171,7 @@ void CSMTools::ReportModel::flagAsReplaced(int index) hint[0] = 'r'; - line.mHint = hint; + line.mHint = std::move(hint); emit dataChanged(this->index(index, 0), this->index(index, columnCount())); } diff --git a/apps/opencs/model/world/actoradapter.cpp b/apps/opencs/model/world/actoradapter.cpp index 0e3725bbb7..37aaf08445 100644 --- a/apps/opencs/model/world/actoradapter.cpp +++ b/apps/opencs/model/world/actoradapter.cpp @@ -468,13 +468,13 @@ namespace CSMWorld if (type == UniversalId::Type_Creature) { // Valid creature record - setupCreature(id, data); + setupCreature(id, std::move(data)); emit actorChanged(id); } else if (type == UniversalId::Type_Npc) { // Valid npc record - setupNpc(id, data); + setupNpc(id, std::move(data)); emit actorChanged(id); } else @@ -665,7 +665,7 @@ namespace CSMWorld RaceDataPtr data = mCachedRaces.get(race); if (data) { - setupRace(race, data); + setupRace(race, std::move(data)); // Race was changed. Need to mark actor dependencies as dirty. // Cannot use markDirtyDependency because that would invalidate // the current iterator. @@ -683,7 +683,7 @@ namespace CSMWorld ActorDataPtr data = mCachedActors.get(actor); if (data) { - setupActor(actor, data); + setupActor(actor, std::move(data)); } } mDirtyActors.clear(); diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 9db6b3b042..83c8de656a 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -15,11 +15,13 @@ #include #include +#include #include #include #include "collectionbase.hpp" #include "columnbase.hpp" +#include "columnimp.hpp" #include "info.hpp" #include "land.hpp" #include "landtexture.hpp" @@ -82,6 +84,17 @@ namespace CSMWorld record.mIndex = index; } + inline ESM::RefId getRecordId(const ESM::MagicEffect& record) + { + return ESM::RefId::stringRefId(CSMWorld::getStringId(record.mId)); + } + + inline void setRecordId(const ESM::RefId& id, ESM::MagicEffect& record) + { + int index = ESM::MagicEffect::indexNameToIndex(id.getRefIdString()); + record.mId = ESM::RefId::index(ESM::REC_MGEF, static_cast(index)); + } + inline ESM::RefId getRecordId(const LandTexture& record) { return ESM::RefId::stringRefId(LandTexture::createUniqueRecordId(record.mPluginIndex, record.mIndex)); @@ -504,7 +517,7 @@ namespace CSMWorld auto record2 = std::make_unique>(); record2->mState = Record::State_ModifiedOnly; - record2->mModified = record; + record2->mModified = std::move(record); insertRecord(std::move(record2), getAppendIndex(id, type), type); } diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index da49caef10..b2ad84966f 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -36,7 +36,7 @@ CSMWorld::TouchCommand::TouchCommand(IdTable& table, const std::string& id, QUnd void CSMWorld::TouchCommand::redo() { - mOld.reset(mTable.getRecord(mId).clone().get()); + mOld = mTable.getRecord(mId).clone(); mChanged = mTable.touchRecord(mId); } @@ -181,9 +181,8 @@ const std::string& CSMWorld::TouchLandCommand::getDestinationId() const void CSMWorld::TouchLandCommand::onRedo() { + mOld = mLands.getRecord(mId).clone(); mChanged = mLands.touchRecord(mId); - if (mChanged) - mOld.reset(mLands.getRecord(mId).clone().get()); } void CSMWorld::TouchLandCommand::onUndo() diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 6322a77e66..ba1f1e5ac3 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -149,7 +149,8 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mResourcesManager.setVFS(mVFS.get()); constexpr double expiryDelay = 0; - mResourceSystem = std::make_unique(mVFS.get(), expiryDelay); + mResourceSystem + = std::make_unique(mVFS.get(), expiryDelay, &mEncoder.getStatelessEncoder()); Shader::ShaderManager::DefineMap defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); @@ -1107,7 +1108,7 @@ void CSMWorld::Data::loadFallbackEntries() newMarker.mModel = model; newMarker.mRecordFlags = 0; auto record = std::make_unique>(); - record->mBase = newMarker; + record->mBase = std::move(newMarker); record->mState = CSMWorld::RecordBase::State_BaseOnly; mReferenceables.appendRecord(std::move(record), CSMWorld::UniversalId::Type_Static); } @@ -1123,7 +1124,7 @@ void CSMWorld::Data::loadFallbackEntries() newMarker.mModel = model; newMarker.mRecordFlags = 0; auto record = std::make_unique>(); - record->mBase = newMarker; + record->mBase = std::move(newMarker); record->mState = CSMWorld::RecordBase::State_BaseOnly; mReferenceables.appendRecord(std::move(record), CSMWorld::UniversalId::Type_Door); } diff --git a/apps/opencs/model/world/idcompletionmanager.cpp b/apps/opencs/model/world/idcompletionmanager.cpp index 263f462b6e..a4fdb4776d 100644 --- a/apps/opencs/model/world/idcompletionmanager.cpp +++ b/apps/opencs/model/world/idcompletionmanager.cpp @@ -117,7 +117,7 @@ void CSMWorld::IdCompletionManager::generateCompleters(CSMWorld::Data& data) completer->setPopup(popup); // The completer takes ownership of the popup completer->setMaxVisibleItems(10); - mCompleters[current->first] = completer; + mCompleters[current->first] = std::move(completer); } } } diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index bfab0193b0..345f6008ec 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -11,6 +11,7 @@ #include #include +#include CSMWorld::Resources::Resources( const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type, const char* const* extensions) diff --git a/apps/opencs/view/doc/adjusterwidget.cpp b/apps/opencs/view/doc/adjusterwidget.cpp index d4cfdc6d3e..a282ebcaff 100644 --- a/apps/opencs/view/doc/adjusterwidget.cpp +++ b/apps/opencs/view/doc/adjusterwidget.cpp @@ -91,7 +91,7 @@ void CSVDoc::AdjusterWidget::setName(const QString& name, bool addon) { // path already points to the local data directory message = "Will be saved as: " + Files::pathToQString(path); - mResultPath = path; + mResultPath = std::move(path); } // in all other cases, ensure the path points to data-local and do an existing file check else diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index dff4426ba5..812a1bd534 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -410,7 +410,7 @@ bool CSVDoc::ViewManager::removeDocument(CSVDoc::View* view) remainingViews.push_back(*iter); } mDocumentManager.removeDocument(document); - mViews = remainingViews; + mViews = std::move(remainingViews); } return true; } diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 3d5c6fe565..214618a627 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -514,7 +514,7 @@ void CSVRender::PagedWorldspaceWidget::moveCellSelection(int x, int y) addCellToScene(*iter); } - mSelection = newSelection; + mSelection = std::move(newSelection); } void CSVRender::PagedWorldspaceWidget::addCellToSceneFromCamera(int offsetX, int offsetY) diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index 79e9959cd6..684958da34 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -541,7 +541,7 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe = landTable.data(landTable.getModelIndex(cellId, textureColumn)) .value(); newTerrainOtherCell[yInOtherCell * landTextureSize + xInOtherCell] = brushInt; - pushEditToCommand(newTerrainOtherCell, document, landTable, cellId); + pushEditToCommand(newTerrainOtherCell, document, landTable, std::move(cellId)); } } } @@ -702,7 +702,7 @@ void CSVRender::TerrainTextureMode::createTexture(const std::string& textureFile QModelIndex index(ltexTable.getModelIndex(newId, ltexTable.findColumnIndex(CSMWorld::Columns::ColumnId_Texture))); undoStack.push(new CSMWorld::ModifyCommand(ltexTable, index, textureFileNameVariant)); undoStack.endMacro(); - mBrushTexture = newId; + mBrushTexture = std::move(newId); } bool CSVRender::TerrainTextureMode::allowLandTextureEditing(const std::string& cellId) diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index da02c1e179..2af84fb36d 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -184,11 +184,11 @@ void CSVRender::WorldspaceWidget::selectDefaultNavigationMode() void CSVRender::WorldspaceWidget::centerOrbitCameraOnSelection() { - std::vector> selection = getSelection(~0u); + std::vector> selection = getSelection(Mask_Reference); for (std::vector>::iterator it = selection.begin(); it != selection.end(); ++it) { - if (CSVRender::ObjectTag* objectTag = dynamic_cast(it->get())) + if (CSVRender::ObjectTag* objectTag = static_cast(it->get())) { mOrbitCamControl->setCenter(objectTag->mObject->getPosition().asVec3()); } @@ -440,7 +440,7 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick( osg::Node* node = *nodeIter; if (osg::ref_ptr tag = dynamic_cast(node->getUserData())) { - WorldspaceHitResult hit = { true, tag, 0, 0, 0, intersection.getWorldIntersectPoint() }; + WorldspaceHitResult hit = { true, std::move(tag), 0, 0, 0, intersection.getWorldIntersectPoint() }; if (intersection.indexList.size() >= 3) { hit.index0 = intersection.indexList[0]; @@ -757,13 +757,14 @@ void CSVRender::WorldspaceWidget::toggleHiddenInstances() if (selection.empty()) return; - const CSVRender::ObjectTag* firstSelection = dynamic_cast(selection.begin()->get()); + const CSVRender::ObjectTag* firstSelection = static_cast(selection.begin()->get()); + assert(firstSelection != nullptr); const CSVRender::Mask firstMask = firstSelection->mObject->getRootNode()->getNodeMask() == Mask_Hidden ? Mask_Reference : Mask_Hidden; for (const auto& object : selection) - if (const auto objectTag = dynamic_cast(object.get())) + if (const auto objectTag = static_cast(object.get())) objectTag->mObject->getRootNode()->setNodeMask(firstMask); } diff --git a/apps/opencs/view/widget/scenetooltexturebrush.cpp b/apps/opencs/view/widget/scenetooltexturebrush.cpp index cc372753f6..9c3e723009 100644 --- a/apps/opencs/view/widget/scenetooltexturebrush.cpp +++ b/apps/opencs/view/widget/scenetooltexturebrush.cpp @@ -213,7 +213,7 @@ void CSVWidget::TextureBrushWindow::setBrushTexture(std::string brushTexture) mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel)); } - mBrushTexture = newBrushTextureId; + mBrushTexture = std::move(newBrushTextureId); emit passTextureId(mBrushTexture); emit passBrushShape(mBrushShape); // updates the icon tooltip diff --git a/apps/opencs/view/world/extendedcommandconfigurator.cpp b/apps/opencs/view/world/extendedcommandconfigurator.cpp index 69659be8a6..97494fa076 100644 --- a/apps/opencs/view/world/extendedcommandconfigurator.cpp +++ b/apps/opencs/view/world/extendedcommandconfigurator.cpp @@ -146,7 +146,7 @@ void CSVWorld::ExtendedCommandConfigurator::setupCheckBoxes(const std::vectorfirst->setText(QString::fromUtf8(type.getTypeName().c_str())); current->first->setChecked(true); - current->second = type; + current->second = std::move(type); ++counter; } else diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index c9e09e2d6a..891d954ad4 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -169,7 +169,7 @@ void CSVWorld::TableSubView::createFilterRequest(std::vectorgetId(); - filterData.columns = col; + filterData.columns = std::move(col); sourceFilter.emplace_back(filterData); } @@ -195,7 +195,7 @@ void CSVWorld::TableSubView::createFilterRequest(std::vector profile(frameStart, frameNumber, *timer, *stats); - - if (mStateManager->getState() != MWBase::StateManager::State_NoGame) - { - mWorld->updateWindowManager(); - } + mWorld->updateWindowManager(); } mLuaWorker->allowUpdate(); // if there is a separate Lua thread, it starts the update now @@ -706,7 +702,8 @@ void OMW::Engine::prepareEngine() VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); - mResourceSystem = std::make_unique(mVFS.get(), Settings::cells().mCacheExpiryDelay); + mResourceSystem = std::make_unique( + mVFS.get(), Settings::cells().mCacheExpiryDelay, &mEncoder.get()->getStatelessEncoder()); mResourceSystem->getSceneManager()->getShaderManager().setMaxTextureUnits(mGlMaxTextureImageUnits); mResourceSystem->getSceneManager()->setUnRefImageDataAfterApply( false); // keep to Off for now to allow better state sharing @@ -826,7 +823,7 @@ void OMW::Engine::prepareEngine() } listener->loadingOff(); - mWorld->init(mViewer, rootNode, mWorkQueue.get(), *mUnrefQueue); + mWorld->init(mViewer, std::move(rootNode), mWorkQueue.get(), *mUnrefQueue); mEnvironment.setWorldScene(mWorld->getWorldScene()); mWorld->setupPlayer(); mWorld->setRandomSeed(mRandomSeed); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index b0b49f3acd..5bbc0211c1 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -219,8 +219,6 @@ int runApplication(int argc, char* argv[]) Platform::init(); #ifdef __APPLE__ - std::filesystem::path binary_path = std::filesystem::absolute(std::filesystem::path(argv[0])); - std::filesystem::current_path(binary_path.parent_path()); setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index 10d6476653..13737bf5ab 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -8,6 +8,7 @@ #include #include "../mwgui/mode.hpp" +#include "../mwrender/animationpriority.hpp" #include namespace MWWorld @@ -61,10 +62,14 @@ namespace MWBase virtual void itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) = 0; virtual void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; virtual void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) = 0; + virtual void animationTextKey(const MWWorld::Ptr& actor, const std::string& key) = 0; + virtual void playAnimation(const MWWorld::Ptr& object, const std::string& groupname, + const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, + std::string_view start, std::string_view stop, float startpoint, size_t loops, bool loopfallback) + = 0; virtual void exteriorCreated(MWWorld::CellStore& cell) = 0; virtual void actorDied(const MWWorld::Ptr& actor) = 0; virtual void questUpdated(const ESM::RefId& questId, int stage) = 0; - // `arg` is either forwarded from MWGui::pushGuiMode or empty virtual void uiModeChanged(const MWWorld::Ptr& arg) = 0; diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index b8e0fd1bde..532100af7a 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -9,6 +9,7 @@ #include #include "../mwmechanics/greetingstate.hpp" +#include "../mwrender/animationpriority.hpp" #include "../mwworld/ptr.hpp" @@ -170,16 +171,33 @@ namespace MWBase ///< Forces an object to refresh its animation state. virtual bool playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number = 1, bool persist = false) + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number = 1, bool scripted = false) = 0; ///< Run animation for a MW-reference. Calls to this function for references that are currently not /// in the scene should be ignored. /// /// \param mode 0 normal, 1 immediate start, 2 immediate loop - /// \param count How many times the animation should be run - /// \param persist Whether the animation state should be stored in saved games - /// and persist after cell unload. + /// \param number How many times the animation should be run + /// \param scripted Whether the animation should be treated as a scripted animation. /// \return Success or error + virtual bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) + = 0; + ///< Lua variant of playAnimationGroup. The mode parameter is omitted + /// and forced to 0. modes 1 and 2 can be emulated by doing clearAnimationQueue() and + /// setting the startKey. + /// + /// \param number How many times the animation should be run + /// \param speed How fast to play the animation, where 1.f = normal speed + /// \param startKey Which textkey to start the animation from + /// \param stopKey Which textkey to stop the animation on + /// \param forceLoop Force the animation to be looping, even if it's normally not looping. + /// \param blendMask See MWRender::Animation::BlendMask + /// \param scripted Whether the animation should be treated as as scripted animation + /// \return Success or error + /// + + virtual void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) = 0; virtual void skipAnimation(const MWWorld::Ptr& ptr) = 0; ///< Skip the animation for the given MW-reference for one frame. Calls to this function for @@ -187,9 +205,14 @@ namespace MWBase virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0; + virtual bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const = 0; + /// Save the current animation state of managed references to their RefData. virtual void persistAnimationStates() = 0; + /// Clear out the animation queue, and cancel any animation currently playing from the queue + virtual void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) = 0; + /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) virtual void updateMagicEffects(const MWWorld::Ptr& ptr) = 0; diff --git a/apps/openmw/mwclass/actor.cpp b/apps/openmw/mwclass/actor.cpp index 9c197a70d2..0a45a85a74 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -37,23 +37,6 @@ namespace MWClass return true; } - void Actor::block(const MWWorld::Ptr& ptr) const - { - const MWWorld::InventoryStore& inv = getInventoryStore(ptr); - MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield == inv.end()) - return; - - MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); - const ESM::RefId skill = shield->getClass().getEquipmentSkill(*shield); - if (skill == ESM::Skill::LightArmor) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); - else if (skill == ESM::Skill::MediumArmor) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f); - else if (skill == ESM::Skill::HeavyArmor) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); - } - osg::Vec3f Actor::getRotationVector(const MWWorld::Ptr& ptr) const { MWMechanics::Movement& movement = getMovementSettings(ptr); diff --git a/apps/openmw/mwclass/actor.hpp b/apps/openmw/mwclass/actor.hpp index 41d06cf5bd..cf0cb1eaa5 100644 --- a/apps/openmw/mwclass/actor.hpp +++ b/apps/openmw/mwclass/actor.hpp @@ -45,8 +45,6 @@ namespace MWClass bool useAnim() const override; - void block(const MWWorld::Ptr& ptr) const override; - osg::Vec3f getRotationVector(const MWWorld::Ptr& ptr) const override; ///< Return desired rotations, as euler angles. Sets getMovementSettings(ptr).mRotation to zero. diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index bb9c1bc277..66a195489e 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -183,16 +183,17 @@ namespace MWClass return getClassModel(ptr); } - void Creature::getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const + void Creature::getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const { std::string model = getModel(ptr); if (!model.empty()) models.push_back(model); - // FIXME: use const version of InventoryStore functions once they are available - if (hasInventoryStore(ptr)) + const MWWorld::CustomData* customData = ptr.getRefData().getCustomData(); + if (customData && hasInventoryStore(ptr)) { - const MWWorld::InventoryStore& invStore = getInventoryStore(ptr); + const auto& invStore + = static_cast(*customData->asCreatureCustomData().mContainerStore); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); @@ -339,10 +340,7 @@ namespace MWClass MWMechanics::applyElementalShields(ptr, victim); if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) - { damage = 0; - victim.getClass().block(victim); - } MWMechanics::diseaseContact(victim, ptr); @@ -513,7 +511,7 @@ namespace MWClass throw std::runtime_error("this creature has no inventory store"); } - bool Creature::hasInventoryStore(const MWWorld::Ptr& ptr) const + bool Creature::hasInventoryStore(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Weapon); } diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index b407852242..38b7bb0ec1 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -79,7 +79,7 @@ namespace MWClass MWWorld::InventoryStore& getInventoryStore(const MWWorld::Ptr& ptr) const override; ///< Return inventory store - bool hasInventoryStore(const MWWorld::Ptr& ptr) const override; + bool hasInventoryStore(const MWWorld::ConstPtr& ptr) const override; ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr @@ -107,7 +107,7 @@ namespace MWClass std::string getModel(const MWWorld::ConstPtr& ptr) const override; - void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; + void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: ///< list getModel(). diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index fbae54737c..f16601531d 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -99,25 +99,6 @@ namespace MWClass customData.mSpawn = true; } - void CreatureLevList::getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const - { - // disable for now, too many false positives - /* - const MWWorld::LiveCellRef *ref = ptr.get(); - for (std::vector::const_iterator it = ref->mBase->mList.begin(); it != - ref->mBase->mList.end(); ++it) - { - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if (it->mLevel > player.getClass().getCreatureStats(player).getLevel()) - continue; - - const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - MWWorld::ManualRef ref(store, it->mId); - ref.getPtr().getClass().getModelsToPreload(ref.getPtr(), models); - } - */ - } - void CreatureLevList::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { diff --git a/apps/openmw/mwclass/creaturelevlist.hpp b/apps/openmw/mwclass/creaturelevlist.hpp index d689d1770e..ded8f77de5 100644 --- a/apps/openmw/mwclass/creaturelevlist.hpp +++ b/apps/openmw/mwclass/creaturelevlist.hpp @@ -20,10 +20,6 @@ namespace MWClass bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; - ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: - ///< list getModel(). - void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index 5225170be7..6bd28103f8 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -143,7 +143,7 @@ namespace MWClass list.push_back(params); } - info.effects = list; + info.effects = std::move(list); info.text = std::move(text); info.isIngredient = true; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 4f295f7b35..de587954b8 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -436,10 +436,11 @@ namespace MWClass return model; } - void Npc::getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const + void Npc::getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const { const MWWorld::LiveCellRef* npc = ptr.get(); - const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().search(npc->mBase->mRace); + const auto& esmStore = MWBase::Environment::get().getESMStore(); + const ESM::Race* race = esmStore->get().search(npc->mBase->mRace); if (race && race->mData.mFlags & ESM::Race::Beast) models.push_back(Settings::models().mBaseanimkna); @@ -453,56 +454,57 @@ namespace MWClass if (!npc->mBase->mHead.empty()) { - const ESM::BodyPart* head - = MWBase::Environment::get().getESMStore()->get().search(npc->mBase->mHead); + const ESM::BodyPart* head = esmStore->get().search(npc->mBase->mHead); if (head) models.push_back(Misc::ResourceHelpers::correctMeshPath(head->mModel)); } if (!npc->mBase->mHair.empty()) { - const ESM::BodyPart* hair - = MWBase::Environment::get().getESMStore()->get().search(npc->mBase->mHair); + const ESM::BodyPart* hair = esmStore->get().search(npc->mBase->mHair); if (hair) models.push_back(Misc::ResourceHelpers::correctMeshPath(hair->mModel)); } bool female = (npc->mBase->mFlags & ESM::NPC::Female); - // FIXME: use const version of InventoryStore functions once they are available - // preload equipped items - const MWWorld::InventoryStore& invStore = getInventoryStore(ptr); - for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + const MWWorld::CustomData* customData = ptr.getRefData().getCustomData(); + if (customData) { - MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); - if (equipped != invStore.end()) + const MWWorld::InventoryStore& invStore = customData->asNpcCustomData().mInventoryStore; + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { - std::vector parts; - if (equipped->getType() == ESM::Clothing::sRecordId) + MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); + if (equipped != invStore.end()) { - const ESM::Clothing* clothes = equipped->get()->mBase; - parts = clothes->mParts.mParts; - } - else if (equipped->getType() == ESM::Armor::sRecordId) - { - const ESM::Armor* armor = equipped->get()->mBase; - parts = armor->mParts.mParts; - } - else - { - std::string model = equipped->getClass().getModel(*equipped); - if (!model.empty()) - models.push_back(model); - } + const auto addParts = [&](const std::vector& parts) { + for (const ESM::PartReference& partRef : parts) + { + const ESM::RefId& partname + = (female && !partRef.mFemale.empty()) || (!female && partRef.mMale.empty()) + ? partRef.mFemale + : partRef.mMale; - for (std::vector::const_iterator it = parts.begin(); it != parts.end(); ++it) - { - const ESM::RefId& partname - = (female && !it->mFemale.empty()) || (!female && it->mMale.empty()) ? it->mFemale : it->mMale; - - const ESM::BodyPart* part - = MWBase::Environment::get().getESMStore()->get().search(partname); - if (part && !part->mModel.empty()) - models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel)); + const ESM::BodyPart* part = esmStore->get().search(partname); + if (part && !part->mModel.empty()) + models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel)); + } + }; + if (equipped->getType() == ESM::Clothing::sRecordId) + { + const ESM::Clothing* clothes = equipped->get()->mBase; + addParts(clothes->mParts.mParts); + } + else if (equipped->getType() == ESM::Armor::sRecordId) + { + const ESM::Armor* armor = equipped->get()->mBase; + addParts(armor->mParts.mParts); + } + else + { + std::string model = equipped->getClass().getModel(*equipped); + if (!model.empty()) + models.push_back(model); + } } } } @@ -512,9 +514,8 @@ namespace MWClass { const std::vector& parts = MWRender::NpcAnimation::getBodyParts(race->mId, female, false, false); - for (std::vector::const_iterator it = parts.begin(); it != parts.end(); ++it) + for (const ESM::BodyPart* part : parts) { - const ESM::BodyPart* part = *it; if (part && !part->mModel.empty()) models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel)); } @@ -678,10 +679,7 @@ namespace MWClass MWMechanics::applyElementalShields(ptr, victim); if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) - { damage = 0; - victim.getClass().block(victim); - } if (victim == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState()) damage = 0; diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index ca0d0ac95d..95245bb994 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -74,7 +74,7 @@ namespace MWClass MWWorld::InventoryStore& getInventoryStore(const MWWorld::Ptr& ptr) const override; ///< Return inventory store - bool hasInventoryStore(const MWWorld::Ptr& ptr) const override { return true; } + bool hasInventoryStore(const MWWorld::ConstPtr& ptr) const override { return true; } bool evaluateHit(const MWWorld::Ptr& ptr, MWWorld::Ptr& victim, osg::Vec3f& hitPosition) const override; @@ -85,7 +85,7 @@ namespace MWClass const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const override; - void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; + void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: ///< list getModel(). diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 4141e61e34..c5280d1615 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -63,17 +63,17 @@ namespace switch (order) { case 0: - return { question, { r0, r1, r2 }, sound }; + return { std::move(question), { std::move(r0), std::move(r1), std::move(r2) }, std::move(sound) }; case 1: - return { question, { r0, r2, r1 }, sound }; + return { std::move(question), { std::move(r0), std::move(r2), std::move(r1) }, std::move(sound) }; case 2: - return { question, { r1, r0, r2 }, sound }; + return { std::move(question), { std::move(r1), std::move(r0), std::move(r2) }, std::move(sound) }; case 3: - return { question, { r1, r2, r0 }, sound }; + return { std::move(question), { std::move(r1), std::move(r2), std::move(r0) }, std::move(sound) }; case 4: - return { question, { r2, r0, r1 }, sound }; + return { std::move(question), { std::move(r2), std::move(r0), std::move(r1) }, std::move(sound) }; default: - return { question, { r2, r1, r0 }, sound }; + return { std::move(question), { std::move(r2), std::move(r1), std::move(r0) }, std::move(sound) }; } } } diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 0e44b8c03e..56f69eb906 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -196,7 +196,7 @@ namespace MWGui std::string topicName = Misc::StringUtils::lowerCase(windowManager->getTranslationDataStorage().topicStandardForm(link)); - std::string displayName = link; + std::string displayName = std::move(link); while (displayName[displayName.size() - 1] == '*') displayName.erase(displayName.size() - 1, 1); @@ -248,7 +248,7 @@ namespace MWGui i = match.mEnd; } if (i != text.end()) - addTopicLink(typesetter, 0, i - text.begin(), text.size()); + addTopicLink(std::move(typesetter), 0, i - text.begin(), text.size()); } } @@ -364,9 +364,8 @@ namespace MWGui if (mCurrentWindowSize == _sender->getSize()) return; - mTopicsList->adjustSize(); + redrawTopicsList(); updateHistory(); - updateTopicFormat(); mCurrentWindowSize = _sender->getSize(); } @@ -534,6 +533,14 @@ namespace MWGui return true; } + void DialogueWindow::redrawTopicsList() + { + mTopicsList->adjustSize(); + + // The topics list has been regenerated so topic formatting needs to be updated + updateTopicFormat(); + } + void DialogueWindow::updateTopicsPane() { mTopicsList->clear(); @@ -591,11 +598,9 @@ namespace MWGui t->eventTopicActivated += MyGUI::newDelegate(this, &DialogueWindow::onTopicActivated); mTopicLinks[topicId] = std::move(t); } - mTopicsList->adjustSize(); + redrawTopicsList(); updateHistory(); - // The topics list has been regenerated so topic formatting needs to be updated - updateTopicFormat(); } void DialogueWindow::updateHistory(bool scrollbar) @@ -756,21 +761,12 @@ namespace MWGui + std::string("/100")); } - bool dispositionWasVisible = mDispositionBar->getVisible(); - - if (dispositionVisible && !dispositionWasVisible) + if (mDispositionBar->getVisible() != dispositionVisible) { - mDispositionBar->setVisible(true); - int offset = mDispositionBar->getHeight() + 5; + mDispositionBar->setVisible(dispositionVisible); + const int offset = (mDispositionBar->getHeight() + 5) * (dispositionVisible ? 1 : -1); mTopicsList->setCoord(mTopicsList->getCoord() + MyGUI::IntCoord(0, offset, 0, -offset)); - mTopicsList->adjustSize(); - } - else if (!dispositionVisible && dispositionWasVisible) - { - mDispositionBar->setVisible(false); - int offset = mDispositionBar->getHeight() + 5; - mTopicsList->setCoord(mTopicsList->getCoord() - MyGUI::IntCoord(0, offset, 0, -offset)); - mTopicsList->adjustSize(); + redrawTopicsList(); } } diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index 1b79cadca5..8a8b309401 100644 --- a/apps/openmw/mwgui/dialogue.hpp +++ b/apps/openmw/mwgui/dialogue.hpp @@ -190,6 +190,7 @@ namespace MWGui void updateDisposition(); void restock(); void deleteLater(); + void redrawTopicsList(); bool mIsCompanion; std::list mKeywords; diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index 1723841b32..8ba2bb8312 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" diff --git a/apps/openmw/mwgui/postprocessorhud.cpp b/apps/openmw/mwgui/postprocessorhud.cpp index ab5bdf791d..6f73c5a9fd 100644 --- a/apps/openmw/mwgui/postprocessorhud.cpp +++ b/apps/openmw/mwgui/postprocessorhud.cpp @@ -136,9 +136,9 @@ namespace MWGui return; if (enabled) - processor->enableTechnique(technique); + processor->enableTechnique(std::move(technique)); else - processor->disableTechnique(technique); + processor->disableTechnique(std::move(technique)); processor->saveChain(); } } @@ -171,7 +171,7 @@ namespace MWGui if (technique->getDynamic()) return; - if (processor->enableTechnique(technique, index) != MWRender::PostProcessor::Status_Error) + if (processor->enableTechnique(std::move(technique), index) != MWRender::PostProcessor::Status_Error) processor->saveChain(); } } diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index fbd54586df..6c6a34595e 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -131,7 +132,7 @@ namespace void updateMaxLightsComboBox(MyGUI::ComboBox* box) { constexpr int min = 8; - constexpr int max = 32; + constexpr int max = 64; constexpr int increment = 8; const int maxLights = Settings::shaders().mMaxLights; // show increments of 8 in dropdown diff --git a/apps/openmw/mwlua/animationbindings.cpp b/apps/openmw/mwlua/animationbindings.cpp new file mode 100644 index 0000000000..272685dc11 --- /dev/null +++ b/apps/openmw/mwlua/animationbindings.cpp @@ -0,0 +1,365 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/character.hpp" + +#include "../mwworld/esmstore.hpp" + +#include "context.hpp" +#include "luamanagerimp.hpp" +#include "objectvariant.hpp" + +#include "animationbindings.hpp" +#include + +namespace MWLua +{ + struct AnimationGroup; + struct TextKeyCallback; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical> : std::false_type + { + }; +} + +namespace MWLua +{ + using BlendMask = MWRender::Animation::BlendMask; + using BoneGroup = MWRender::Animation::BoneGroup; + using Priority = MWMechanics::Priority; + using AnimationPriorities = MWRender::Animation::AnimPriority; + + MWWorld::Ptr getMutablePtrOrThrow(const ObjectVariant& variant) + { + if (variant.isLObject()) + throw std::runtime_error("Local scripts can only modify animations of the object they are attached to."); + + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + if (!ptr.getRefData().isEnabled()) + throw std::runtime_error("Can't use a disabled object"); + + return ptr; + } + + MWWorld::Ptr getPtrOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + + return ptr; + } + + MWRender::Animation* getMutableAnimationOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = getMutablePtrOrThrow(variant); + auto world = MWBase::Environment::get().getWorld(); + MWRender::Animation* anim = world->getAnimation(ptr); + if (!anim) + throw std::runtime_error("Object has no animation"); + return anim; + } + + const MWRender::Animation* getConstAnimationOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = getPtrOrThrow(variant); + auto world = MWBase::Environment::get().getWorld(); + const MWRender::Animation* anim = world->getAnimation(ptr); + if (!anim) + throw std::runtime_error("Object has no animation"); + return anim; + } + + const ESM::Static* getStatic(const sol::object& staticOrID) + { + if (staticOrID.is()) + return staticOrID.as(); + else + { + ESM::RefId id = ESM::RefId::deserializeText(LuaUtil::cast(staticOrID)); + return MWBase::Environment::get().getWorld()->getStore().get().find(id); + } + } + + std::string getStaticModelOrThrow(const sol::object& staticOrID) + { + const ESM::Static* static_ = getStatic(staticOrID); + if (!static_) + throw std::runtime_error("Invalid static"); + + return Misc::ResourceHelpers::correctMeshPath(static_->mModel); + } + + static AnimationPriorities getPriorityArgument(const sol::table& args) + { + auto asPriorityEnum = args.get>("priority"); + if (asPriorityEnum) + return asPriorityEnum.value(); + + auto asTable = args.get>("priority"); + if (asTable) + { + AnimationPriorities priorities = AnimationPriorities(Priority::Priority_Default); + for (auto entry : asTable.value()) + { + if (!entry.first.is() || !entry.second.is()) + throw std::runtime_error("Priority table must consist of BoneGroup-Priority pairs only"); + auto group = entry.first.as(); + auto priority = entry.second.as(); + if (group < 0 || group >= BoneGroup::Num_BoneGroups) + throw std::runtime_error("Invalid bonegroup: " + std::to_string(group)); + priorities[group] = priority; + } + + return priorities; + } + + return Priority::Priority_Default; + } + + sol::table initAnimationPackage(const Context& context) + { + auto* lua = context.mLua; + auto mechanics = MWBase::Environment::get().getMechanicsManager(); + auto world = MWBase::Environment::get().getWorld(); + + sol::table api(lua->sol(), sol::create); + + api["PRIORITY"] + = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + { "Default", MWMechanics::Priority::Priority_Default }, + { "WeaponLowerBody", MWMechanics::Priority::Priority_WeaponLowerBody }, + { "SneakIdleLowerBody", MWMechanics::Priority::Priority_SneakIdleLowerBody }, + { "SwimIdle", MWMechanics::Priority::Priority_SwimIdle }, + { "Jump", MWMechanics::Priority::Priority_Jump }, + { "Movement", MWMechanics::Priority::Priority_Movement }, + { "Hit", MWMechanics::Priority::Priority_Hit }, + { "Weapon", MWMechanics::Priority::Priority_Weapon }, + { "Block", MWMechanics::Priority::Priority_Block }, + { "Knockdown", MWMechanics::Priority::Priority_Knockdown }, + { "Torch", MWMechanics::Priority::Priority_Torch }, + { "Storm", MWMechanics::Priority::Priority_Storm }, + { "Death", MWMechanics::Priority::Priority_Death }, + { "Scripted", MWMechanics::Priority::Priority_Scripted }, + })); + + api["BLEND_MASK"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + { "LowerBody", BlendMask::BlendMask_LowerBody }, + { "Torso", BlendMask::BlendMask_Torso }, + { "LeftArm", BlendMask::BlendMask_LeftArm }, + { "RightArm", BlendMask::BlendMask_RightArm }, + { "UpperBody", BlendMask::BlendMask_UpperBody }, + { "All", BlendMask::BlendMask_All }, + })); + + api["BONE_GROUP"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + { "LowerBody", BoneGroup::BoneGroup_LowerBody }, + { "Torso", BoneGroup::BoneGroup_Torso }, + { "LeftArm", BoneGroup::BoneGroup_LeftArm }, + { "RightArm", BoneGroup::BoneGroup_RightArm }, + })); + + api["hasAnimation"] = [world](const sol::object& object) -> bool { + return world->getAnimation(getPtrOrThrow(ObjectVariant(object))) != nullptr; + }; + + // equivalent to MWScript's SkipAnim + api["skipAnimationThisFrame"] = [mechanics](const sol::object& object) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + // This sets a flag that is only used during the update pass, so + // there's no need to queue + mechanics->skipAnimation(ptr); + }; + + api["getTextKeyTime"] = [](const sol::object& object, std::string_view key) -> sol::optional { + float time = getConstAnimationOrThrow(ObjectVariant(object))->getTextKeyTime(key); + if (time >= 0.f) + return time; + return sol::nullopt; + }; + api["isPlaying"] = [](const sol::object& object, std::string_view groupname) { + return getConstAnimationOrThrow(ObjectVariant(object))->isPlaying(groupname); + }; + api["getCurrentTime"] = [](const sol::object& object, std::string_view groupname) -> sol::optional { + float time = getConstAnimationOrThrow(ObjectVariant(object))->getCurrentTime(groupname); + if (time >= 0.f) + return time; + return sol::nullopt; + }; + api["isLoopingAnimation"] = [](const sol::object& object, std::string_view groupname) { + return getConstAnimationOrThrow(ObjectVariant(object))->isLoopingAnimation(groupname); + }; + api["cancel"] = [](const sol::object& object, std::string_view groupname) { + return getMutableAnimationOrThrow(ObjectVariant(object))->disable(groupname); + }; + api["setLoopingEnabled"] = [](const sol::object& object, std::string_view groupname, bool enabled) { + return getMutableAnimationOrThrow(ObjectVariant(object))->setLoopingEnabled(groupname, enabled); + }; + // MWRender::Animation::getInfo can also return the current speed multiplier, but this is never used. + api["getCompletion"] = [](const sol::object& object, std::string_view groupname) -> sol::optional { + float completion = 0.f; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, &completion)) + return completion; + return sol::nullopt; + }; + api["getLoopCount"] = [](const sol::object& object, std::string groupname) -> sol::optional { + size_t loops = 0; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, nullptr, &loops)) + return loops; + return sol::nullopt; + }; + api["getSpeed"] = [](const sol::object& object, std::string groupname) -> sol::optional { + float speed = 0.f; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, &speed, nullptr)) + return speed; + return sol::nullopt; + }; + api["setSpeed"] = [](const sol::object& object, std::string groupname, float speed) { + getMutableAnimationOrThrow(ObjectVariant(object))->adjustSpeedMult(groupname, speed); + }; + api["getActiveGroup"] = [](const sol::object& object, MWRender::BoneGroup boneGroup) -> std::string_view { + return getConstAnimationOrThrow(ObjectVariant(object))->getActiveGroup(boneGroup); + }; + + // Clears out the animation queue, and cancel any animation currently playing from the queue + api["clearAnimationQueue"] = [mechanics](const sol::object& object, bool clearScripted) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->clearAnimationQueue(ptr, clearScripted); + }; + + // Extended variant of MWScript's PlayGroup and LoopGroup + api["playQueued"] = sol::overload( + [mechanics](const sol::object& object, const std::string& groupname, const sol::table& options) { + int numberOfLoops = options.get_or("loops", std::numeric_limits::max()); + float speed = options.get_or("speed", 1.f); + std::string startKey = options.get_or("startkey", "start"); + std::string stopKey = options.get_or("stopkey", "stop"); + bool forceLoop = options.get_or("forceloop", false); + + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->playAnimationGroupLua(ptr, groupname, numberOfLoops, speed, startKey, stopKey, forceLoop); + }, + [mechanics](const sol::object& object, const std::string& groupname) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->playAnimationGroupLua( + ptr, groupname, std::numeric_limits::max(), 1, "start", "stop", false); + }); + + api["playBlended"] = [](const sol::object& object, std::string_view groupname, const sol::table& options) { + int loops = options.get_or("loops", 0); + MWRender::Animation::AnimPriority priority = getPriorityArgument(options); + BlendMask blendMask = options.get_or("blendmask", BlendMask::BlendMask_All); + bool autoDisable = options.get_or("autodisable", true); + float speed = options.get_or("speed", 1.0f); + std::string start = options.get_or("startkey", "start"); + std::string stop = options.get_or("stopkey", "stop"); + float startpoint = options.get_or("startpoint", 0.0f); + bool forceLoop = options.get_or("forceloop", false); + + auto animation = getMutableAnimationOrThrow(ObjectVariant(object)); + animation->play(groupname, priority, blendMask, autoDisable, speed, start, stop, startpoint, loops, + forceLoop || animation->isLoopingAnimation(groupname)); + }; + + api["hasGroup"] = [](const sol::object& object, std::string_view groupname) -> bool { + const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object)); + return anim->hasAnimation(groupname); + }; + + // Note: This checks the nodemap, and does not read the scene graph itself, and so should be thread safe. + api["hasBone"] = [](const sol::object& object, std::string_view bonename) -> bool { + const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object)); + return anim->getNode(bonename) != nullptr; + }; + + api["addVfx"] = sol::overload( + [context](const sol::object& object, const sol::object& staticOrID) { + context.mLuaManager->addAction( + [object = ObjectVariant(object), model = getStaticModelOrThrow(staticOrID)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->addEffect(model, ""); + }, + "addVfxAction"); + }, + [context](const sol::object& object, const sol::object& staticOrID, const sol::table& options) { + context.mLuaManager->addAction( + [object = ObjectVariant(object), model = getStaticModelOrThrow(staticOrID), + effectId = options.get_or("vfxId", ""), loop = options.get_or("loop", false), + bonename = options.get_or("bonename", ""), + particleTexture = options.get_or("particleTextureOverride", "")] { + MWRender::Animation* anim = getMutableAnimationOrThrow(ObjectVariant(object)); + + anim->addEffect(model, effectId, loop, bonename, particleTexture); + }, + "addVfxAction"); + }); + + api["removeVfx"] = [context](const sol::object& object, std::string_view effectId) { + context.mLuaManager->addAction( + [object = ObjectVariant(object), effectId = std::string(effectId)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->removeEffect(effectId); + }, + "removeVfxAction"); + }; + + api["removeAllVfx"] = [context](const sol::object& object) { + context.mLuaManager->addAction( + [object = ObjectVariant(object)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->removeEffects(); + }, + "removeVfxAction"); + }; + + return LuaUtil::makeReadOnly(api); + } + + sol::table initCoreVfxBindings(const Context& context) + { + sol::state_view& lua = context.mLua->sol(); + sol::table api(lua, sol::create); + auto world = MWBase::Environment::get().getWorld(); + + api["spawn"] = sol::overload( + [world, context](const sol::object& staticOrID, const osg::Vec3f& worldPos) { + auto model = getStaticModelOrThrow(staticOrID); + context.mLuaManager->addAction( + [world, model, worldPos]() { world->spawnEffect(model, "", worldPos); }, "openmw.vfx.spawn"); + }, + [world, context](const sol::object& staticOrID, const osg::Vec3f& worldPos, const sol::table& options) { + auto model = getStaticModelOrThrow(staticOrID); + + bool magicVfx = options.get_or("mwMagicVfx", true); + std::string textureOverride = options.get_or("particleTextureOverride", ""); + float scale = options.get_or("scale", 1.f); + + context.mLuaManager->addAction( + [world, model, textureOverride, worldPos, scale, magicVfx]() { + world->spawnEffect(model, textureOverride, worldPos, scale, magicVfx); + }, + "openmw.vfx.spawn"); + }); + + return api; + } +} diff --git a/apps/openmw/mwlua/animationbindings.hpp b/apps/openmw/mwlua/animationbindings.hpp new file mode 100644 index 0000000000..d28dda9208 --- /dev/null +++ b/apps/openmw/mwlua/animationbindings.hpp @@ -0,0 +1,12 @@ +#ifndef MWLUA_ANIMATIONBINDINGS_H +#define MWLUA_ANIMATIONBINDINGS_H + +#include + +namespace MWLua +{ + sol::table initAnimationPackage(const Context& context); + sol::table initCoreVfxBindings(const Context& context); +} + +#endif // MWLUA_ANIMATIONBINDINGS_H diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp index 081df13a0e..963a437f62 100644 --- a/apps/openmw/mwlua/cellbindings.cpp +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -115,6 +115,13 @@ namespace MWLua return cell == c.mStore || (cell->getCell()->getWorldSpace() == c.mStore->getCell()->getWorldSpace()); }; + cellT["waterLevel"] = sol::readonly_property([](const CellT& c) -> sol::optional { + if (c.mStore->getCell()->hasWater()) + return c.mStore->getWaterLevel(); + else + return sol::nullopt; + }); + if constexpr (std::is_same_v) { // only for global scripts cellT["getAll"] = [ids = getPackageToTypeTable(context.mLua->sol())]( @@ -270,7 +277,7 @@ namespace MWLua if (!ok) throw std::runtime_error( std::string("Incorrect type argument in cell:getAll: " + LuaUtil::toString(*type))); - return GObjectList{ res }; + return GObjectList{ std::move(res) }; }; } } diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp index 62f22d0992..8c881f9f75 100644 --- a/apps/openmw/mwlua/corebindings.cpp +++ b/apps/openmw/mwlua/corebindings.cpp @@ -17,6 +17,7 @@ #include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" +#include "animationbindings.hpp" #include "factionbindings.hpp" #include "luaevents.hpp" #include "magicbindings.hpp" @@ -83,6 +84,7 @@ namespace MWLua }; api["contentFiles"] = initContentFilesBindings(lua->sol()); api["sound"] = initCoreSoundBindings(context); + api["vfx"] = initCoreVfxBindings(context); api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string { const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); for (size_t i = 0; i < contentList.size(); ++i) @@ -133,6 +135,7 @@ namespace MWLua api[k] = v; api["sendGlobalEvent"] = sol::nil; api["sound"] = sol::nil; + api["vfx"] = sol::nil; return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwlua/engineevents.cpp b/apps/openmw/mwlua/engineevents.cpp index 0fbb13f1cf..43507ff1a5 100644 --- a/apps/openmw/mwlua/engineevents.cpp +++ b/apps/openmw/mwlua/engineevents.cpp @@ -86,6 +86,15 @@ namespace MWLua void operator()(const OnNewExterior& event) const { mGlobalScripts.onNewExterior(GCell{ &event.mCell }); } + void operator()(const OnAnimationTextKey& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onAnimationTextKey(event.mGroupname, event.mKey); + } + private: MWWorld::Ptr getPtr(ESM::RefNum id) const { diff --git a/apps/openmw/mwlua/engineevents.hpp b/apps/openmw/mwlua/engineevents.hpp index 7c706edcd0..bf8d219fd5 100644 --- a/apps/openmw/mwlua/engineevents.hpp +++ b/apps/openmw/mwlua/engineevents.hpp @@ -51,7 +51,14 @@ namespace MWLua { MWWorld::CellStore& mCell; }; - using Event = std::variant; + struct OnAnimationTextKey + { + ESM::RefNum mActor; + std::string mGroupname; + std::string mKey; + }; + using Event = std::variant; void clear() { mQueue.clear(); } void addToQueue(Event e) { mQueue.push_back(std::move(e)); } diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index 3aaf101a6b..ae54061cb6 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -82,13 +82,18 @@ namespace MWLua inputActions[sol::meta_function::pairs] = pairs; } - context.mLua->sol().new_usertype("ActionInfo", "key", - sol::property([](const LuaUtil::InputAction::Info& info) { return info.mKey; }), "name", - sol::property([](const LuaUtil::InputAction::Info& info) { return info.mName; }), "description", - sol::property([](const LuaUtil::InputAction::Info& info) { return info.mDescription; }), "type", - sol::property([](const LuaUtil::InputAction::Info& info) { return info.mType; }), "l10n", - sol::property([](const LuaUtil::InputAction::Info& info) { return info.mL10n; }), "defaultValue", - sol::property([](const LuaUtil::InputAction::Info& info) { return info.mDefaultValue; })); + auto actionInfo = context.mLua->sol().new_usertype("ActionInfo"); + actionInfo["key"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mKey; }); + actionInfo["name"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mName; }); + actionInfo["description"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mDescription; }); + actionInfo["l10n"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mL10n; }); + actionInfo["type"] = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mType; }); + actionInfo["defaultValue"] + = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mDefaultValue; }); auto inputTriggers = context.mLua->sol().new_usertype("InputTriggers"); inputTriggers[sol::meta_function::index] @@ -108,11 +113,15 @@ namespace MWLua inputTriggers[sol::meta_function::pairs] = pairs; } - context.mLua->sol().new_usertype("TriggerInfo", "key", - sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mKey; }), "name", - sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mName; }), "description", - sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mDescription; }), "l10n", - sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mL10n; })); + auto triggerInfo = context.mLua->sol().new_usertype("TriggerInfo"); + triggerInfo["key"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mKey; }); + triggerInfo["name"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mName; }); + triggerInfo["description"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mDescription; }); + triggerInfo["l10n"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mL10n; }); MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); sol::table api(context.mLua->sol(), sol::create); diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 8cf383e985..1d5e710869 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -3,6 +3,8 @@ #include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/aicombat.hpp" #include "../mwmechanics/aiescort.hpp" #include "../mwmechanics/aifollow.hpp" @@ -162,6 +164,10 @@ namespace MWLua MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); ai.stack(MWMechanics::AiTravel(target.x(), target.y(), target.z(), false), ptr, cancelOther); }; + selfAPI["_enableLuaAnimations"] = [](SelfObject& self, bool enable) { + const MWWorld::Ptr& ptr = self.ptr(); + MWBase::Environment::get().getMechanicsManager()->enableLuaAnimations(ptr, enable); + }; } LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) @@ -170,7 +176,7 @@ namespace MWLua { this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); registerEngineHandlers({ &mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers, &mOnActivatedHandlers, - &mOnTeleportedHandlers }); + &mOnTeleportedHandlers, &mOnAnimationTextKeyHandlers, &mOnPlayAnimationHandlers }); } void LocalScripts::setActive(bool active) diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index b87b628a89..230ec93d3c 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -71,6 +71,14 @@ namespace MWLua void onConsume(const LObject& consumable) { callEngineHandlers(mOnConsumeHandlers, consumable); } void onActivated(const LObject& actor) { callEngineHandlers(mOnActivatedHandlers, actor); } void onTeleported() { callEngineHandlers(mOnTeleportedHandlers); } + void onAnimationTextKey(std::string_view groupname, std::string_view key) + { + callEngineHandlers(mOnAnimationTextKeyHandlers, groupname, key); + } + void onPlayAnimation(std::string_view groupname, const sol::table& options) + { + callEngineHandlers(mOnPlayAnimationHandlers, groupname, options); + } void applyStatsCache(); @@ -83,6 +91,8 @@ namespace MWLua EngineHandlerList mOnConsumeHandlers{ "onConsume" }; EngineHandlerList mOnActivatedHandlers{ "onActivated" }; EngineHandlerList mOnTeleportedHandlers{ "onTeleported" }; + EngineHandlerList mOnAnimationTextKeyHandlers{ "_onAnimationTextKey" }; + EngineHandlerList mOnPlayAnimationHandlers{ "_onPlayAnimation" }; }; } diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index a7269d6e52..0de10827e0 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -7,6 +7,7 @@ #include "../mwbase/world.hpp" #include "../mwworld/datetimemanager.hpp" +#include "animationbindings.hpp" #include "camerabindings.hpp" #include "cellbindings.hpp" #include "corebindings.hpp" @@ -30,6 +31,7 @@ namespace MWLua sol::state_view lua = context.mLua->sol(); MWWorld::DateTimeManager* tm = MWBase::Environment::get().getWorld()->getTimeManager(); return { + { "openmw.animation", initAnimationPackage(context) }, { "openmw.async", LuaUtil::getAsyncPackageInitializer( lua, [tm] { return tm->getSimulationTime(); }, [tm] { return tm->getGameTime(); }) }, diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 6e62c8b7e3..f695bd294e 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -24,6 +24,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwrender/bonegroup.hpp" #include "../mwrender/postprocessor.hpp" #include "../mwworld/datetimemanager.hpp" @@ -402,6 +403,49 @@ namespace MWLua mEngineEvents.addToQueue(EngineEvents::OnUseItem{ getId(actor), getId(object), force }); } + void LuaManager::animationTextKey(const MWWorld::Ptr& actor, const std::string& key) + { + auto pos = key.find(": "); + if (pos != std::string::npos) + mEngineEvents.addToQueue( + EngineEvents::OnAnimationTextKey{ getId(actor), key.substr(0, pos), key.substr(pos + 2) }); + } + + void LuaManager::playAnimation(const MWWorld::Ptr& actor, const std::string& groupname, + const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, + std::string_view start, std::string_view stop, float startpoint, size_t loops, bool loopfallback) + { + sol::table options = mLua.newTable(); + options["blendmask"] = blendMask; + options["autodisable"] = autodisable; + options["speed"] = speedmult; + options["startkey"] = start; + options["stopkey"] = stop; + options["startpoint"] = startpoint; + options["loops"] = loops; + options["forceloop"] = loopfallback; + + bool priorityAsTable = false; + for (uint32_t i = 1; i < MWRender::sNumBlendMasks; i++) + if (priority[static_cast(i)] != priority[static_cast(0)]) + priorityAsTable = true; + if (priorityAsTable) + { + sol::table priorityTable = mLua.newTable(); + for (uint32_t i = 0; i < MWRender::sNumBlendMasks; i++) + priorityTable[static_cast(i)] = priority[static_cast(i)]; + options["priority"] = priorityTable; + } + else + options["priority"] = priority[MWRender::BoneGroup_LowerBody]; + + // mEngineEvents.addToQueue(event); + // Has to be called immediately, otherwise engine details that depend on animations playing immediately + // break. + if (auto* scripts = actor.getRefData().getLuaScripts()) + scripts->onPlayAnimation(groupname, options); + } + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 8ae83308d4..e82c503c3a 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -82,6 +82,10 @@ namespace MWLua mEngineEvents.addToQueue(EngineEvents::OnActivate{ getId(actor), getId(object) }); } void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) override; + void animationTextKey(const MWWorld::Ptr& actor, const std::string& key) override; + void playAnimation(const MWWorld::Ptr& actor, const std::string& groupname, + const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, + std::string_view start, std::string_view stop, float startpoint, size_t loops, bool loopfallback) override; void exteriorCreated(MWWorld::CellStore& cell) override { mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 3d57ab24fc..1e3cb2ab69 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -389,6 +389,17 @@ namespace MWLua auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); + magicEffectT["particle"] + = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return rec.mParticle; }); + magicEffectT["continuousVfx"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> bool { + return (rec.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; + }); + magicEffectT["castingStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mCasting.serializeText(); }); + magicEffectT["hitStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mHit.serializeText(); }); + magicEffectT["areaStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mArea.serializeText(); }); magicEffectT["name"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return MWBase::Environment::get() .getWorld() @@ -730,7 +741,7 @@ namespace MWLua auto id = sol::make_object(lua, self.mIterator->getId().serializeText()); auto params = sol::make_object(lua, ActiveSpell{ self.mActor, *self.mIterator }); self.advance(); - return { params, params }; + return { id, params }; } else { diff --git a/apps/openmw/mwlua/mwscriptbindings.cpp b/apps/openmw/mwlua/mwscriptbindings.cpp index af88249d3e..a41ef30a44 100644 --- a/apps/openmw/mwlua/mwscriptbindings.cpp +++ b/apps/openmw/mwlua/mwscriptbindings.cpp @@ -104,8 +104,12 @@ namespace MWLua }); mwscript["player"] = sol::readonly_property( [](const MWScriptRef&) { return GObject(MWBase::Environment::get().getWorld()->getPlayerPtr()); }); - mwscriptVars[sol::meta_function::index] = [](MWScriptVariables& s, std::string_view var) { - return s.mRef.getLocals().getVarAsDouble(s.mRef.mId, Misc::StringUtils::lowerCase(var)); + mwscriptVars[sol::meta_function::index] + = [](MWScriptVariables& s, std::string_view var) -> sol::optional { + if (s.mRef.getLocals().hasVar(s.mRef.mId, var)) + return s.mRef.getLocals().getVarAsDouble(s.mRef.mId, Misc::StringUtils::lowerCase(var)); + else + return sol::nullopt; }; mwscriptVars[sol::meta_function::new_index] = [](MWScriptVariables& s, std::string_view var, double val) { MWScript::Locals& locals = s.mRef.getLocals(); diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 748d963bdc..47c55e86f0 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -613,7 +613,7 @@ namespace MWLua MWBase::Environment::get().getWorldModel()->registerPtr(item); list->push_back(getId(item)); } - return ObjectList{ list }; + return ObjectList{ std::move(list) }; }; inventoryT["countOf"] = [](const InventoryT& inventory, std::string_view recordId) { @@ -661,7 +661,7 @@ namespace MWLua list->push_back(getId(item)); } } - return ObjectList{ list }; + return ObjectList{ std::move(list) }; }; } diff --git a/apps/openmw/mwlua/soundbindings.cpp b/apps/openmw/mwlua/soundbindings.cpp index 55071ea374..e8b7089eb8 100644 --- a/apps/openmw/mwlua/soundbindings.cpp +++ b/apps/openmw/mwlua/soundbindings.cpp @@ -23,6 +23,11 @@ namespace float mTimeOffset = 0.f; }; + struct StreamMusicArgs + { + float mFade = 1.f; + }; + PlaySoundArgs getPlaySoundArgs(const sol::optional& options) { PlaySoundArgs args; @@ -55,6 +60,17 @@ namespace return MWSound::PlayMode::NoEnvNoScaling; return MWSound::PlayMode::NoEnv; } + + StreamMusicArgs getStreamMusicArgs(const sol::optional& options) + { + StreamMusicArgs args; + + if (options.has_value()) + { + args.mFade = options->get_or("fadeOut", 1.f); + } + return args; + } } namespace MWLua @@ -99,9 +115,10 @@ namespace MWLua return MWBase::Environment::get().getSoundManager()->getSoundPlaying(MWWorld::Ptr(), fileName); }; - api["streamMusic"] = [](std::string_view fileName) { + api["streamMusic"] = [](std::string_view fileName, const sol::optional& options) { + auto args = getStreamMusicArgs(options); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->streamMusic(std::string(fileName), MWSound::MusicType::Scripted); + sndMgr->streamMusic(std::string(fileName), MWSound::MusicType::Scripted, args.mFade); }; api["isMusicPlaying"] = []() { return MWBase::Environment::get().getSoundManager()->isMusicPlaying(); }; diff --git a/apps/openmw/mwlua/stats.cpp b/apps/openmw/mwlua/stats.cpp index ff74704ed8..02bed00bf5 100644 --- a/apps/openmw/mwlua/stats.cpp +++ b/apps/openmw/mwlua/stats.cpp @@ -380,7 +380,7 @@ namespace MWLua addProp(context, attributeStatT, "base", &MWMechanics::AttributeValue::getBase); addProp(context, attributeStatT, "damage", &MWMechanics::AttributeValue::getDamage); attributeStatT["modified"] - = sol::property([=](const AttributeStat& stat) { return stat.getModified(context); }); + = sol::readonly_property([=](const AttributeStat& stat) { return stat.getModified(context); }); addProp(context, attributeStatT, "modifier", &MWMechanics::AttributeValue::getModifier); sol::table attributes(context.mLua->sol(), sol::create); stats["attributes"] = LuaUtil::makeReadOnly(attributes); @@ -399,7 +399,8 @@ namespace MWLua auto skillStatT = context.mLua->sol().new_usertype("SkillStat"); addProp(context, skillStatT, "base", &MWMechanics::SkillValue::getBase); addProp(context, skillStatT, "damage", &MWMechanics::SkillValue::getDamage); - skillStatT["modified"] = sol::property([=](const SkillStat& stat) { return stat.getModified(context); }); + skillStatT["modified"] + = sol::readonly_property([=](const SkillStat& stat) { return stat.getModified(context); }); addProp(context, skillStatT, "modifier", &MWMechanics::SkillValue::getModifier); skillStatT["progress"] = sol::property([context](const SkillStat& stat) { return stat.getProgress(context); }, [context](const SkillStat& stat, const sol::object& value) { stat.cache(context, "progress", value); }); diff --git a/apps/openmw/mwlua/types/creature.cpp b/apps/openmw/mwlua/types/creature.cpp index ddf90bf8c5..dd4b1bd67b 100644 --- a/apps/openmw/mwlua/types/creature.cpp +++ b/apps/openmw/mwlua/types/creature.cpp @@ -1,3 +1,4 @@ +#include "../stats.hpp" #include "actor.hpp" #include "types.hpp" @@ -42,6 +43,20 @@ namespace MWLua record["soulValue"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mSoul; }); record["type"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mType; }); record["baseGold"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mGold; }); + record["combatSkill"] + = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mCombat; }); + record["magicSkill"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mMagic; }); + record["stealthSkill"] + = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mStealth; }); + record["attack"] = sol::readonly_property([context](const ESM::Creature& rec) -> sol::table { + sol::state_view& lua = context.mLua->sol(); + sol::table res(lua, sol::create); + int index = 1; + for (auto attack : rec.mData.mAttack) + res[index++] = attack; + return LuaUtil::makeReadOnly(res); + }); + addActorServicesBindings(record, context); } } diff --git a/apps/openmw/mwlua/vfsbindings.cpp b/apps/openmw/mwlua/vfsbindings.cpp index 0eccb336c2..c9b1a45fe2 100644 --- a/apps/openmw/mwlua/vfsbindings.cpp +++ b/apps/openmw/mwlua/vfsbindings.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index d8e409d9e2..a9c669fce5 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -324,7 +324,7 @@ namespace MWMechanics MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation && !reflectStatic->mModel.empty()) animation->addEffect(Misc::ResourceHelpers::correctMeshPath(reflectStatic->mModel), - ESM::MagicEffect::Reflect, false); + ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); } if (removedSpell) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 25c4c97504..92f8a212c9 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -2019,6 +2019,24 @@ namespace MWMechanics return false; } } + + bool Actors::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->getCharacterController().playGroupLua( + groupName, speed, startKey, stopKey, loops, forceLoop); + return false; + } + + void Actors::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().enableLuaAnimations(enable); + } + void Actors::skipAnimation(const MWWorld::Ptr& ptr) const { const auto iter = mIndex.find(ptr.mRef); @@ -2034,12 +2052,27 @@ namespace MWMechanics return false; } + bool Actors::checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->getCharacterController().isScriptedAnimPlaying(); + return false; + } + void Actors::persistAnimationStates() const { for (const Actor& actor : mActors) actor.getCharacterController().persistAnimationState(); } + void Actors::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().clearAnimQueue(clearScripted); + } + void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { for (const Actor& actor : mActors) diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 15a39136a6..3ead5f069a 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -114,9 +114,14 @@ namespace MWMechanics bool playAnimationGroup( const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false) const; + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop); + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable); void skipAnimation(const MWWorld::Ptr& ptr) const; bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const; + bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const; void persistAnimationStates() const; + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index a265c70cf4..fe83ce11ab 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -9,6 +9,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/luamanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" @@ -120,12 +121,12 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& MWBase::World* world = MWBase::Environment::get().getWorld(); const DetourNavigator::AgentBounds agentBounds = world->getPathfindingAgentBounds(actor); - /// Stops the actor when it gets too close to a unloaded cell - //... At current time, this test is unnecessary. AI shuts down when actor is more than "actors processing range" - // setting value - //... units from player, and exterior cells are 8192 units long and wide. + /// Stops the actor when it gets too close to a unloaded cell or when the actor is playing a scripted animation + //... At current time, the first test is unnecessary. AI shuts down when actor is more than + //... "actors processing range" setting value units from player, and exterior cells are 8192 units long and wide. //... But AI processing distance may increase in the future. - if (isNearInactiveCell(position)) + if (isNearInactiveCell(position) + || MWBase::Environment::get().getMechanicsManager()->checkScriptedAnimationPlaying(actor)) { actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 20c7fd0a92..5533cb578b 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -36,6 +36,7 @@ #include "../mwrender/animation.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -270,7 +271,7 @@ namespace case CharState_IdleSwim: return Priority_SwimIdle; case CharState_IdleSneak: - priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; + priority[MWRender::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; [[fallthrough]]; default: return priority; @@ -444,8 +445,8 @@ namespace MWMechanics { mHitState = CharState_Block; priority = Priority_Hit; - priority[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block; - priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + priority[MWRender::BoneGroup_LeftArm] = Priority_Block; + priority[MWRender::BoneGroup_LowerBody] = Priority_WeaponLowerBody; startKey = "block start"; stopKey = "block stop"; } @@ -482,8 +483,7 @@ namespace MWMechanics return; } - mAnimation->play( - mCurrentHit, priority, MWRender::Animation::BlendMask_All, true, 1, startKey, stopKey, 0.0f, ~0ul); + playBlendedAnimation(mCurrentHit, priority, MWRender::BlendMask_All, true, 1, startKey, stopKey, 0.0f, ~0ul); } void CharacterController::refreshJumpAnims(JumpingState jump, bool force) @@ -502,7 +502,7 @@ namespace MWMechanics std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); std::string jumpAnimName = "jump"; jumpAnimName += weapShortGroup; - MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All; + MWRender::Animation::BlendMask jumpmask = MWRender::BlendMask_All; if (!weapShortGroup.empty() && !mAnimation->hasAnimation(jumpAnimName)) jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask); @@ -520,10 +520,10 @@ namespace MWMechanics mCurrentJump = jumpAnimName; if (mJumpState == JumpState_InAir) - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, startAtLoop ? "loop start" : "start", - "stop", 0.f, ~0ul); + playBlendedAnimation(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, + startAtLoop ? "loop start" : "start", "stop", 0.f, ~0ul); else if (mJumpState == JumpState_Landing) - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0); + playBlendedAnimation(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0); } bool CharacterController::onOpen() const @@ -539,8 +539,8 @@ namespace MWMechanics if (mAnimation->isPlaying("containerclose")) return false; - mAnimation->play("containeropen", Priority_Scripted, MWRender::Animation::BlendMask_All, false, 1.0f, - "start", "stop", 0.f, 0); + mAnimation->play( + "containeropen", Priority_Scripted, MWRender::BlendMask_All, false, 1.0f, "start", "stop", 0.f, 0); if (mAnimation->isPlaying("containeropen")) return false; } @@ -560,8 +560,8 @@ namespace MWMechanics if (animPlaying) startPoint = 1.f - complete; - mAnimation->play("containerclose", Priority_Scripted, MWRender::Animation::BlendMask_All, false, 1.0f, - "start", "stop", startPoint, 0); + mAnimation->play("containerclose", Priority_Scripted, MWRender::BlendMask_All, false, 1.0f, "start", "stop", + startPoint, 0); } } @@ -600,7 +600,7 @@ namespace MWMechanics if (!isRealWeapon(mWeaponType)) { if (blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + *blendMask = MWRender::BlendMask_LowerBody; return baseGroupName; } @@ -619,13 +619,13 @@ namespace MWMechanics // Special case for crossbows - we should apply 1h animations a fallback only for lower body if (mWeaponType == ESM::Weapon::MarksmanCrossbow && blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + *blendMask = MWRender::BlendMask_LowerBody; if (!mAnimation->hasAnimation(groupName)) { groupName = baseGroupName; if (blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + *blendMask = MWRender::BlendMask_LowerBody; } return groupName; @@ -658,7 +658,7 @@ namespace MWMechanics } } - MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All; + MWRender::Animation::BlendMask movemask = MWRender::BlendMask_All; std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); @@ -684,7 +684,7 @@ namespace MWMechanics if (!mAnimation->hasAnimation(weapMovementAnimName)) weapMovementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); - movementAnimName = weapMovementAnimName; + movementAnimName = std::move(weapMovementAnimName); } if (!mAnimation->hasAnimation(movementAnimName)) @@ -749,7 +749,7 @@ namespace MWMechanics } } - mAnimation->play( + playBlendedAnimation( mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true); } @@ -798,7 +798,7 @@ namespace MWMechanics weapIdleGroup += weapShortGroup; if (!mAnimation->hasAnimation(weapIdleGroup)) weapIdleGroup = fallbackShortWeaponGroup(idleGroup); - idleGroup = weapIdleGroup; + idleGroup = std::move(weapIdleGroup); // play until the Loop Stop key 2 to 5 times, then play until the Stop key // this replicates original engine behavior for the "Idle1h" 1st-person animation @@ -820,9 +820,9 @@ namespace MWMechanics mAnimation->getInfo(mCurrentIdle, &startPoint); clearStateAnimation(mCurrentIdle); - mCurrentIdle = idleGroup; - mAnimation->play(mCurrentIdle, priority, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", - startPoint, numLoops, true); + mCurrentIdle = std::move(idleGroup); + playBlendedAnimation( + mCurrentIdle, priority, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true); } void CharacterController::refreshCurrentAnims( @@ -855,8 +855,8 @@ namespace MWMechanics resetCurrentIdleState(); resetCurrentJumpState(); - mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All, false, 1.0f, "start", - "stop", startpoint, 0); + playBlendedAnimation( + mCurrentDeath, Priority_Death, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startpoint, 0); } CharacterState CharacterController::chooseRandomDeathState() const @@ -998,6 +998,8 @@ namespace MWMechanics { std::string_view evt = key->second; + MWBase::Environment::get().getLuaManager()->animationTextKey(mPtr, key->second); + if (evt.substr(0, 7) == "sound: ") { MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); @@ -1189,8 +1191,9 @@ namespace MWMechanics { if (!animPlaying) { - int mask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm; - mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul, true); + int mask = MWRender::BlendMask_Torso | MWRender::BlendMask_RightArm; + playBlendedAnimation( + "idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul, true); } else { @@ -1247,41 +1250,6 @@ namespace MWMechanics } } - bool CharacterController::isLoopingAnimation(std::string_view group) const - { - // In Morrowind, a some animation groups are always considered looping, regardless - // of loop start/stop keys. - // To be match vanilla behavior we probably only need to check this list, but we don't - // want to prevent modded animations with custom group names from looping either. - static const std::unordered_set loopingAnimations = { "walkforward", "walkback", "walkleft", - "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", "runforward", "runback", - "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", "sneakforward", - "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright", - "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", - "idle8", "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", - "inventoryweapononehand", "inventoryweapontwohand", "inventoryweapontwowide" }; - static const std::vector shortGroups = getAllWeaponTypeShortGroups(); - - if (mAnimation && mAnimation->getTextKeyTime(std::string(group) + ": loop start") >= 0) - return true; - - // Most looping animations have variants for each weapon type shortgroup. - // Just remove the shortgroup instead of enumerating all of the possible animation groupnames. - // Make sure we pick the longest shortgroup so e.g. "bow" doesn't get picked over "crossbow" - // when the shortgroup is crossbow. - std::size_t suffixLength = 0; - for (std::string_view suffix : shortGroups) - { - if (suffix.length() > suffixLength && group.ends_with(suffix)) - { - suffixLength = suffix.length(); - } - } - group.remove_suffix(suffixLength); - - return loopingAnimations.count(group) > 0; - } - bool CharacterController::updateWeaponState() { // If the current animation is scripted, we can't do anything here. @@ -1357,8 +1325,8 @@ namespace MWMechanics if (mAnimation->isPlaying("shield")) mAnimation->disable("shield"); - mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm, false, 1.0f, "start", - "stop", 0.0f, std::numeric_limits::max(), true); + playBlendedAnimation("torch", Priority_Torch, MWRender::BlendMask_LeftArm, false, 1.0f, "start", "stop", + 0.0f, std::numeric_limits::max(), true); } else if (mAnimation->isPlaying("torch")) { @@ -1369,7 +1337,7 @@ namespace MWMechanics // For biped actors, blend weapon animations with lower body animations with higher priority MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); if (cls.isBipedal(mPtr)) - priorityWeapon[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + priorityWeapon[MWRender::BoneGroup_LowerBody] = Priority_WeaponLowerBody; bool forcestateupdate = false; @@ -1400,19 +1368,19 @@ namespace MWMechanics { // Note: we do not disable unequipping animation automatically to avoid body desync weapgroup = getWeaponAnimation(mWeaponType); - int unequipMask = MWRender::Animation::BlendMask_All; + int unequipMask = MWRender::BlendMask_All; bool useShieldAnims = mAnimation->useShieldAnimations(); if (useShieldAnims && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && !(mWeaponType == ESM::Weapon::None && weaptype == ESM::Weapon::Spell)) { - unequipMask = unequipMask | ~MWRender::Animation::BlendMask_LeftArm; - mAnimation->play("shield", Priority_Block, MWRender::Animation::BlendMask_LeftArm, true, 1.0f, + unequipMask = unequipMask | ~MWRender::BlendMask_LeftArm; + playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f, "unequip start", "unequip stop", 0.0f, 0); } else if (mWeaponType == ESM::Weapon::HandToHand) mAnimation->showCarriedLeft(false); - mAnimation->play( + playBlendedAnimation( weapgroup, priorityWeapon, unequipMask, false, 1.0f, "unequip start", "unequip stop", 0.0f, 0); mUpperBodyState = UpperBodyState::Unequipping; @@ -1458,15 +1426,15 @@ namespace MWMechanics if (weaptype != ESM::Weapon::None) { mAnimation->showWeapons(false); - int equipMask = MWRender::Animation::BlendMask_All; + int equipMask = MWRender::BlendMask_All; if (useShieldAnims && weaptype != ESM::Weapon::Spell) { - equipMask = equipMask | ~MWRender::Animation::BlendMask_LeftArm; - mAnimation->play("shield", Priority_Block, MWRender::Animation::BlendMask_LeftArm, true, - 1.0f, "equip start", "equip stop", 0.0f, 0); + equipMask = equipMask | ~MWRender::BlendMask_LeftArm; + playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f, + "equip start", "equip stop", 0.0f, 0); } - mAnimation->play( + playBlendedAnimation( weapgroup, priorityWeapon, equipMask, true, 1.0f, "equip start", "equip stop", 0.0f, 0); mUpperBodyState = UpperBodyState::Equipping; @@ -1617,11 +1585,11 @@ namespace MWMechanics if (mAnimation->getNode("Bip01 L Hand")) mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), - -1, false, "Bip01 L Hand", effect->mParticle); + "", false, "Bip01 L Hand", effect->mParticle); if (mAnimation->getNode("Bip01 R Hand")) mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), - -1, false, "Bip01 R Hand", effect->mParticle); + "", false, "Bip01 R Hand", effect->mParticle); } // first effect used for casting animation const ESM::ENAMstruct& firstEffect = effects->front(); @@ -1656,9 +1624,8 @@ namespace MWMechanics startKey = mAttackType + " start"; stopKey = mAttackType + " stop"; } - - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, - 1, startKey, stopKey, 0.0f, 0); + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, 1, + startKey, stopKey, 0.0f, 0); mUpperBodyState = UpperBodyState::Casting; } } @@ -1709,8 +1676,8 @@ namespace MWMechanics mAttackVictim = MWWorld::Ptr(); mAttackHitPos = osg::Vec3f(); - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, - weapSpeed, startKey, stopKey, 0.0f, 0); + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, + startKey, stopKey, 0.0f, 0); } } @@ -1783,7 +1750,7 @@ namespace MWMechanics } mAnimation->disable(mCurrentWeapon); - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, weapSpeed, + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, mAttackType + " max attack", mAttackType + ' ' + hit, startPoint, 0); } @@ -1813,7 +1780,7 @@ namespace MWMechanics // Follow animations have lower priority than movement for non-biped creatures, logic be damned if (!cls.isBipedal(mPtr)) priorityFollow = Priority_Default; - mAnimation->play(mCurrentWeapon, priorityFollow, MWRender::Animation::BlendMask_All, false, weapSpeed, + playBlendedAnimation(mCurrentWeapon, priorityFollow, MWRender::BlendMask_All, false, weapSpeed, mAttackType + ' ' + start, mAttackType + ' ' + stop, 0.0f, 0); mUpperBodyState = UpperBodyState::AttackEnd; @@ -1935,9 +1902,16 @@ namespace MWMechanics mIdleState = CharState_SpecialIdle; auto priority = mAnimQueue.front().mScripted ? Priority_Scripted : Priority_Default; mAnimation->setPlayScriptedOnly(mAnimQueue.front().mScripted); - mAnimation->play(mAnimQueue.front().mGroup, priority, MWRender::Animation::BlendMask_All, false, 1.0f, - (loopStart ? "loop start" : "start"), "stop", mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, - mAnimQueue.front().mLooping); + if (mAnimQueue.front().mScripted) + mAnimation->play(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false, + mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey), + mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, + mAnimQueue.front().mLooping); + else + playBlendedAnimation(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false, + mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey), + mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, + mAnimQueue.front().mLooping); } } @@ -2504,6 +2478,7 @@ namespace MWMechanics state.mScriptedAnims.clear(); for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) { + // TODO: Probably want to presist lua animations too if (!iter->mScripted) continue; @@ -2541,8 +2516,10 @@ namespace MWMechanics AnimationQueueEntry entry; entry.mGroup = iter->mGroup; entry.mLoopCount = iter->mLoopCount; - entry.mScripted = true; - entry.mLooping = isLoopingAnimation(entry.mGroup); + entry.mLooping = mAnimation->isLoopingAnimation(entry.mGroup); + entry.mStartKey = "start"; + entry.mStopKey = "stop"; + entry.mSpeed = 1.f; entry.mTime = iter->mTime; if (iter->mAbsolute) { @@ -2559,6 +2536,18 @@ namespace MWMechanics } } + void CharacterController::playBlendedAnimation(const std::string& groupname, const MWRender::AnimPriority& priority, + int blendMask, bool autodisable, float speedmult, std::string_view start, std::string_view stop, + float startpoint, size_t loops, bool loopfallback) const + { + if (mLuaAnimations) + MWBase::Environment::get().getLuaManager()->playAnimation(mPtr, groupname, priority, blendMask, autodisable, + speedmult, start, stop, startpoint, loops, loopfallback); + else + mAnimation->play( + groupname, priority, blendMask, autodisable, speedmult, start, stop, startpoint, loops, loopfallback); + } + bool CharacterController::playGroup(std::string_view groupname, int mode, int count, bool scripted) { if (!mAnimation || !mAnimation->hasAnimation(groupname)) @@ -2568,7 +2557,7 @@ namespace MWMechanics if (isScriptedAnimPlaying() && !scripted) return true; - bool looping = isLoopingAnimation(groupname); + bool looping = mAnimation->isLoopingAnimation(groupname); // If this animation is a looped animation that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count @@ -2602,8 +2591,12 @@ namespace MWMechanics entry.mGroup = groupname; entry.mLoopCount = count; entry.mTime = 0.f; - entry.mScripted = scripted; + // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing + entry.mScripted = (scripted && groupname != "idle"); entry.mLooping = looping; + entry.mSpeed = 1.f; + entry.mStartKey = ((mode == 2) ? "loop start" : "start"); + entry.mStopKey = "stop"; bool playImmediately = false; @@ -2618,10 +2611,6 @@ namespace MWMechanics mAnimQueue.resize(1); } - // "PlayGroup idle" is a special case, used to stop and remove scripted animations playing - if (groupname == "idle") - entry.mScripted = false; - mAnimQueue.push_back(entry); if (playImmediately) @@ -2630,6 +2619,42 @@ namespace MWMechanics return true; } + bool CharacterController::playGroupLua(std::string_view groupname, float speed, std::string_view startKey, + std::string_view stopKey, int loops, bool forceLoop) + { + // Note: In mwscript, "idle" is a special case used to clear the anim queue. + // In lua we offer an explicit clear method instead so this method does not treat "idle" special. + + if (!mAnimation || !mAnimation->hasAnimation(groupname)) + return false; + + AnimationQueueEntry entry; + entry.mGroup = groupname; + // Note: MWScript gives one less loop to actors than non-actors. + // But this is the Lua version. We don't need to reproduce this weirdness here. + entry.mLoopCount = std::max(loops, 0); + entry.mStartKey = startKey; + entry.mStopKey = stopKey; + entry.mLooping = mAnimation->isLoopingAnimation(groupname) || forceLoop; + entry.mScripted = true; + entry.mSpeed = speed; + entry.mTime = 0; + + if (mAnimQueue.size() > 1) + mAnimQueue.resize(1); + mAnimQueue.push_back(entry); + + if (mAnimQueue.size() == 1) + playAnimQueue(); + + return true; + } + + void CharacterController::enableLuaAnimations(bool enable) + { + mLuaAnimations = enable; + } + void CharacterController::skipAnim() { mSkipAnim = true; @@ -2745,18 +2770,20 @@ namespace MWMechanics // as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here. // Stop any effects that are no longer active - std::vector effects; - mAnimation->getLoopingEffects(effects); + std::vector effects = mAnimation->getLoopingEffects(); - for (int effectId : effects) + for (std::string_view effectId : effects) { - if (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() - || mPtr.getClass() - .getCreatureStats(mPtr) - .getMagicEffects() - .getOrDefault(MWMechanics::EffectKey(effectId)) - .getMagnitude() - <= 0) + auto index = ESM::MagicEffect::indexNameToIndex(effectId); + + if (index >= 0 + && (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() + || mPtr.getClass() + .getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(MWMechanics::EffectKey(index)) + .getMagnitude() + <= 0)) mAnimation->removeEffect(effectId); } } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index ee26b61a25..a507c73743 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -138,9 +138,13 @@ namespace MWMechanics float mTime; bool mLooping; bool mScripted; + std::string mStartKey; + std::string mStopKey; + float mSpeed; }; typedef std::deque AnimationQueue; AnimationQueue mAnimQueue; + bool mLuaAnimations{ false }; CharacterState mIdleState{ CharState_None }; std::string mCurrentIdle; @@ -209,15 +213,12 @@ namespace MWMechanics void refreshMovementAnims(CharacterState movement, bool force = false); void refreshIdleAnims(CharacterState idle, bool force = false); - void clearAnimQueue(bool clearScriptedAnims = false); - bool updateWeaponState(); void updateIdleStormState(bool inwater) const; std::string chooseRandomAttackAnimation() const; static bool isRandomAttackAnimation(std::string_view group); - bool isScriptedAnimPlaying() const; bool isMovementAnimationControlled() const; void updateAnimQueue(); @@ -248,8 +249,6 @@ namespace MWMechanics void prepareHit(); - bool isLoopingAnimation(std::string_view group) const; - public: CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim); virtual ~CharacterController(); @@ -275,9 +274,17 @@ namespace MWMechanics void persistAnimationState() const; void unpersistAnimationState(); + void playBlendedAnimation(const std::string& groupname, const MWRender::AnimPriority& priority, int blendMask, + bool autodisable, float speedmult, std::string_view start, std::string_view stop, float startpoint, + size_t loops, bool loopfallback = false) const; bool playGroup(std::string_view groupname, int mode, int count, bool scripted = false); + bool playGroupLua(std::string_view groupname, float speed, std::string_view startKey, std::string_view stopKey, + int loops, bool forceLoop); + void enableLuaAnimations(bool enable); void skipAnim(); bool isAnimPlaying(std::string_view groupName) const; + bool isScriptedAnimPlaying() const; + void clearAnimQueue(bool clearScriptedAnims = false); enum KillResult { diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 3f17df96fd..3208ea2293 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -135,6 +135,15 @@ namespace MWMechanics auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (Misc::Rng::roll0to99(prng) < x) { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + const ESM::RefId skill = shield->getClass().getEquipmentSkill(*shield); + if (skill == ESM::Skill::LightArmor) + sndMgr->playSound3D(blocker, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); + else if (skill == ESM::Skill::MediumArmor) + sndMgr->playSound3D(blocker, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f); + else if (skill == ESM::Skill::HeavyArmor) + sndMgr->playSound3D(blocker, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); + // Reduce shield durability by incoming damage int shieldhealth = shield->getClass().getItemHealth(*shield); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 59b1392dc9..5323f7e65c 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -756,6 +756,21 @@ namespace MWMechanics else return mObjects.playAnimationGroup(ptr, groupName, mode, number, scripted); } + bool MechanicsManager::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, + float speed, std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + if (ptr.getClass().isActor()) + return mActors.playAnimationGroupLua(ptr, groupName, loops, speed, startKey, stopKey, forceLoop); + else + return mObjects.playAnimationGroupLua(ptr, groupName, loops, speed, startKey, stopKey, forceLoop); + } + void MechanicsManager::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + if (ptr.getClass().isActor()) + mActors.enableLuaAnimations(ptr, enable); + else + mObjects.enableLuaAnimations(ptr, enable); + } void MechanicsManager::skipAnimation(const MWWorld::Ptr& ptr) { if (ptr.getClass().isActor()) @@ -771,6 +786,14 @@ namespace MWMechanics return false; } + bool MechanicsManager::checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const + { + if (ptr.getClass().isActor()) + return mActors.checkScriptedAnimationPlaying(ptr); + + return false; + } + bool MechanicsManager::onOpen(const MWWorld::Ptr& ptr) { if (ptr.getClass().isActor()) @@ -791,6 +814,14 @@ namespace MWMechanics mObjects.persistAnimationStates(); } + void MechanicsManager::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + if (ptr.getClass().isActor()) + mActors.clearAnimationQueue(ptr, clearScripted); + else + mObjects.clearAnimationQueue(ptr, clearScripted); + } + void MechanicsManager::updateMagicEffects(const MWWorld::Ptr& ptr) { mActors.updateMagicEffects(ptr); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 997636522e..93c1fa3dc2 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -143,9 +143,14 @@ namespace MWMechanics /// @return Success or error bool playAnimationGroup( const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false) override; + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) override; + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) override; void skipAnimation(const MWWorld::Ptr& ptr) override; bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) override; + bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const override; void persistAnimationStates() override; + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) override; /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) diff --git a/apps/openmw/mwmechanics/objects.cpp b/apps/openmw/mwmechanics/objects.cpp index 5bdfc91ac7..32d484df2f 100644 --- a/apps/openmw/mwmechanics/objects.cpp +++ b/apps/openmw/mwmechanics/objects.cpp @@ -113,6 +113,23 @@ namespace MWMechanics return false; } } + + bool Objects::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->playGroupLua(groupName, speed, startKey, stopKey, loops, forceLoop); + return false; + } + + void Objects::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->enableLuaAnimations(enable); + } + void Objects::skipAnimation(const MWWorld::Ptr& ptr) { const auto iter = mIndex.find(ptr.mRef); @@ -126,6 +143,13 @@ namespace MWMechanics object.persistAnimationState(); } + void Objects::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->clearAnimQueue(clearScripted); + } + void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { for (const CharacterController& object : mObjects) diff --git a/apps/openmw/mwmechanics/objects.hpp b/apps/openmw/mwmechanics/objects.hpp index 296f454e4f..1fe43530b0 100644 --- a/apps/openmw/mwmechanics/objects.hpp +++ b/apps/openmw/mwmechanics/objects.hpp @@ -47,8 +47,12 @@ namespace MWMechanics bool playAnimationGroup( const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false); + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop); + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable); void skipAnimation(const MWWorld::Ptr& ptr); void persistAnimationStates(); + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index e4e07b162f..0496033c70 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -552,8 +552,8 @@ namespace MWMechanics MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); if (animation) { - animation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), effect->mIndex, false, - {}, effect->mParticle); + animation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + ESM::MagicEffect::indexToName(effect->mIndex), false, {}, effect->mParticle); } else { @@ -626,8 +626,8 @@ namespace MWMechanics { // Don't play particle VFX unless the effect is new or it should be looping. if (playNonLooping || loop) - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), magicEffect.mIndex, loop, - {}, magicEffect.mParticle); + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + ESM::MagicEffect::indexToName(magicEffect.mIndex), loop, {}, magicEffect.mParticle); } } } diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index e7146f3e7a..ebf9933cfc 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -285,8 +285,8 @@ namespace const ESM::Static* absorbStatic = esmStore.get().find(ESM::RefId::stringRefId("VFX_Absorb")); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); if (animation && !absorbStatic->mModel.empty()) - animation->addEffect( - Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel), ESM::MagicEffect::SpellAbsorption, false); + animation->addEffect(Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel), + ESM::MagicEffect::indexToName(ESM::MagicEffect::SpellAbsorption), false); const ESM::Spell* spell = esmStore.get().search(spellId); int spellCost = 0; if (spell) @@ -455,11 +455,11 @@ namespace MWMechanics if (!caster.isEmpty()) { MWRender::Animation* anim = world->getAnimation(caster); - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); const ESM::Static* fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_end")); if (fx) - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), -1); + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), ""); } } else if (caster == getPlayer()) @@ -490,7 +490,7 @@ namespace MWMechanics if (!caster.isEmpty()) { MWRender::Animation* anim = world->getAnimation(caster); - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); } } } @@ -1045,7 +1045,7 @@ namespace MWMechanics effect.mFlags |= ESM::ActiveEffect::Flag_Remove; auto anim = world->getAnimation(target); if (anim) - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); } else effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; @@ -1287,7 +1287,7 @@ namespace MWMechanics { auto anim = MWBase::Environment::get().getWorld()->getAnimation(target); if (anim) - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); } } diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index 85a8d971a9..e4b9e953aa 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -105,7 +105,7 @@ namespace MWMechanics const ESM::Static* fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_Start")); if (fx) - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), -1, false); + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", false); } } catch (std::exception& e) diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index e31a1eb711..2c70cd0436 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -101,7 +101,7 @@ namespace MWRender templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager(), &rotation); } return SceneUtil::attach( - templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); + std::move(templateNode), mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); } std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 7fa8e43c37..581d2843ab 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include @@ -45,6 +46,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" @@ -52,6 +54,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwmechanics/character.hpp" // FIXME: for MWMechanics::Priority +#include "../mwmechanics/weapontype.hpp" #include "actorutil.hpp" #include "rotatecontroller.hpp" @@ -300,11 +303,10 @@ namespace RemoveCallbackVisitor() : RemoveVisitor() , mHasMagicEffects(false) - , mEffectId(-1) { } - RemoveCallbackVisitor(int effectId) + RemoveCallbackVisitor(std::string_view effectId) : RemoveVisitor() , mHasMagicEffects(false) , mEffectId(effectId) @@ -323,7 +325,7 @@ namespace MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { - bool toRemove = mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId; + bool toRemove = mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId; if (toRemove) mToRemove.emplace_back(group.asNode(), group.getParent(0)); else @@ -337,7 +339,7 @@ namespace void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; class FindVfxCallbacksVisitor : public osg::NodeVisitor @@ -347,11 +349,10 @@ namespace FindVfxCallbacksVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mEffectId(-1) { } - FindVfxCallbacksVisitor(int effectId) + FindVfxCallbacksVisitor(std::string_view effectId) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mEffectId(effectId) { @@ -367,7 +368,7 @@ namespace MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { - if (mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId) + if (mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId) { mCallbacks.push_back(vfxCallback); } @@ -381,7 +382,7 @@ namespace void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; osg::ref_ptr getVFXLightModelInstance() @@ -446,7 +447,7 @@ namespace MWRender typedef std::map> ControllerMap; - ControllerMap mControllerMap[Animation::sNumBlendMasks]; + ControllerMap mControllerMap[sNumBlendMasks]; const SceneUtil::TextKeyMap& getTextKeys() const; }; @@ -714,6 +715,41 @@ namespace MWRender return mSupportedAnimations.find(anim) != mSupportedAnimations.end(); } + bool Animation::isLoopingAnimation(std::string_view group) const + { + // In Morrowind, a some animation groups are always considered looping, regardless + // of loop start/stop keys. + // To be match vanilla behavior we probably only need to check this list, but we don't + // want to prevent modded animations with custom group names from looping either. + static const std::unordered_set loopingAnimations = { "walkforward", "walkback", "walkleft", + "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", "runforward", "runback", + "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", "sneakforward", + "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright", + "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", + "idle8", "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", + "inventoryweapononehand", "inventoryweapontwohand", "inventoryweapontwowide" }; + static const std::vector shortGroups = MWMechanics::getAllWeaponTypeShortGroups(); + + if (getTextKeyTime(std::string(group) + ": loop start") >= 0) + return true; + + // Most looping animations have variants for each weapon type shortgroup. + // Just remove the shortgroup instead of enumerating all of the possible animation groupnames. + // Make sure we pick the longest shortgroup so e.g. "bow" doesn't get picked over "crossbow" + // when the shortgroup is crossbow. + std::size_t suffixLength = 0; + for (std::string_view suffix : shortGroups) + { + if (suffix.length() > suffixLength && group.ends_with(suffix)) + { + suffixLength = suffix.length(); + } + } + group.remove_suffix(suffixLength); + + return loopingAnimations.count(group) > 0; + } + float Animation::getStartTime(const std::string& groupname) const { for (AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) @@ -757,16 +793,14 @@ namespace MWRender state.mLoopStopTime = key->first; } - if (mTextKeyListener) + try { - try - { + if (mTextKeyListener != nullptr) mTextKeyListener->handleTextKey(groupname, key, map); - } - catch (std::exception& e) - { - Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); - } + } + catch (std::exception& e) + { + Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); } } @@ -922,7 +956,7 @@ namespace MWRender return true; } - void Animation::setTextKeyListener(Animation::TextKeyListener* listener) + void Animation::setTextKeyListener(TextKeyListener* listener) { mTextKeyListener = listener; } @@ -1051,7 +1085,16 @@ namespace MWRender return true; } - float Animation::getCurrentTime(const std::string& groupname) const + std::string_view Animation::getActiveGroup(BoneGroup boneGroup) const + { + if (auto timePtr = mAnimationTimePtr[boneGroup]->getTimePtr()) + for (auto& state : mStates) + if (state.second.mTime == timePtr) + return state.first; + return ""; + } + + float Animation::getCurrentTime(std::string_view groupname) const { AnimStateMap::const_iterator iter = mStates.find(groupname); if (iter == mStates.end()) @@ -1495,8 +1538,8 @@ namespace MWRender mExtraLightSource->setActorFade(mAlpha); } - void Animation::addEffect( - const std::string& model, int effectId, bool loop, std::string_view bonename, std::string_view texture) + void Animation::addEffect(std::string_view model, std::string_view effectId, bool loop, std::string_view bonename, + std::string_view texture) { if (!mObjectRoot.get()) return; @@ -1578,7 +1621,7 @@ namespace MWRender overrideFirstRootTexture(texture, mResourceSystem, *node); } - void Animation::removeEffect(int effectId) + void Animation::removeEffect(std::string_view effectId) { RemoveCallbackVisitor visitor(effectId); mInsert->accept(visitor); @@ -1588,17 +1631,19 @@ namespace MWRender void Animation::removeEffects() { - removeEffect(-1); + removeEffect(""); } - void Animation::getLoopingEffects(std::vector& out) const + std::vector Animation::getLoopingEffects() const { if (!mHasMagicEffects) - return; + return {}; FindVfxCallbacksVisitor visitor; mInsert->accept(visitor); + std::vector out; + for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) { @@ -1607,6 +1652,7 @@ namespace MWRender if (callback->mParams.mLoop && !callback->mFinished) out.push_back(callback->mParams.mEffectId); } + return out; } void Animation::updateEffects() diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 24366889c4..dae81592b3 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -1,6 +1,10 @@ #ifndef GAME_RENDER_ANIMATION_H #define GAME_RENDER_ANIMATION_H +#include "animationpriority.hpp" +#include "blendmask.hpp" +#include "bonegroup.hpp" + #include "../mwworld/movementdirection.hpp" #include "../mwworld/ptr.hpp" @@ -84,7 +88,7 @@ namespace MWRender std::string mModelName; // Just here so we don't add the same effect twice std::shared_ptr mAnimTime; float mMaxControllerLength; - int mEffectId; + std::string mEffectId; bool mLoop; std::string mBoneName; }; @@ -92,60 +96,9 @@ namespace MWRender class Animation : public osg::Referenced { public: - enum BoneGroup - { - BoneGroup_LowerBody = 0, - BoneGroup_Torso, - BoneGroup_LeftArm, - BoneGroup_RightArm - }; - - enum BlendMask - { - BlendMask_LowerBody = 1 << 0, - BlendMask_Torso = 1 << 1, - BlendMask_LeftArm = 1 << 2, - BlendMask_RightArm = 1 << 3, - - BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm, - - BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody - }; - /* This is the number of *discrete* blend masks. */ - static constexpr size_t sNumBlendMasks = 4; - - /// Holds an animation priority value for each BoneGroup. - struct AnimPriority - { - /// Convenience constructor, initialises all priorities to the same value. - AnimPriority(int priority) - { - for (unsigned int i = 0; i < sNumBlendMasks; ++i) - mPriority[i] = priority; - } - - bool operator==(const AnimPriority& other) const - { - for (unsigned int i = 0; i < sNumBlendMasks; ++i) - if (other.mPriority[i] != mPriority[i]) - return false; - return true; - } - - int& operator[](BoneGroup n) { return mPriority[n]; } - - const int& operator[](BoneGroup n) const { return mPriority[n]; } - - bool contains(int priority) const - { - for (unsigned int i = 0; i < sNumBlendMasks; ++i) - if (priority == mPriority[i]) - return true; - return false; - } - - int mPriority[sNumBlendMasks]; - }; + using BlendMask = MWRender::BlendMask; + using BoneGroup = MWRender::BoneGroup; + using AnimPriority = MWRender::AnimPriority; class TextKeyListener { @@ -384,11 +337,11 @@ namespace MWRender * @param texture override the texture specified in the model's materials - if empty, do not override * @note Will not add an effect twice. */ - void addEffect(const std::string& model, int effectId, bool loop = false, std::string_view bonename = {}, - std::string_view texture = {}); - void removeEffect(int effectId); + void addEffect(std::string_view model, std::string_view effectId, bool loop = false, + std::string_view bonename = {}, std::string_view texture = {}); + void removeEffect(std::string_view effectId); void removeEffects(); - void getLoopingEffects(std::vector& out) const; + std::vector getLoopingEffects() const; // Add a spell casting glow to an object. From measuring video taken from the original engine, // the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second. @@ -398,6 +351,8 @@ namespace MWRender bool hasAnimation(std::string_view anim) const; + bool isLoopingAnimation(std::string_view group) const; + // Specifies the axis' to accumulate on. Non-accumulated axis will just // move visually, but not affect the actual movement. Each x/y/z value // should be on the scale of 0 to 1. @@ -446,6 +401,9 @@ namespace MWRender bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr, size_t* loopcount = nullptr) const; + /// Returns the group name of the animation currently active on that bone group. + std::string_view getActiveGroup(BoneGroup boneGroup) const; + /// Get the absolute position in the animation track of the first text key with the given group. float getStartTime(const std::string& groupname) const; @@ -454,7 +412,7 @@ namespace MWRender /// Get the current absolute position in the animation track for the animation that is currently playing from /// the given group. - float getCurrentTime(const std::string& groupname) const; + float getCurrentTime(std::string_view groupname) const; /** Disables the specified animation group; * \param groupname Animation group to disable. diff --git a/apps/openmw/mwrender/animationpriority.hpp b/apps/openmw/mwrender/animationpriority.hpp new file mode 100644 index 0000000000..048d29901e --- /dev/null +++ b/apps/openmw/mwrender/animationpriority.hpp @@ -0,0 +1,42 @@ +#ifndef GAME_RENDER_ANIMATIONPRIORITY_H +#define GAME_RENDER_ANIMATIONPRIORITY_H + +#include "blendmask.hpp" +#include "bonegroup.hpp" + +namespace MWRender +{ + /// Holds an animation priority value for each BoneGroup. + struct AnimPriority + { + /// Convenience constructor, initialises all priorities to the same value. + AnimPriority(int priority) + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + mPriority[i] = priority; + } + + bool operator==(const AnimPriority& other) const + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + if (other.mPriority[i] != mPriority[i]) + return false; + return true; + } + + int& operator[](BoneGroup n) { return mPriority[n]; } + + const int& operator[](BoneGroup n) const { return mPriority[n]; } + + bool contains(int priority) const + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + if (priority == mPriority[i]) + return true; + return false; + } + + int mPriority[sNumBlendMasks]; + }; +} +#endif diff --git a/apps/openmw/mwrender/blendmask.hpp b/apps/openmw/mwrender/blendmask.hpp new file mode 100644 index 0000000000..f140814d8d --- /dev/null +++ b/apps/openmw/mwrender/blendmask.hpp @@ -0,0 +1,22 @@ +#ifndef GAME_RENDER_BLENDMASK_H +#define GAME_RENDER_BLENDMASK_H + +#include + +namespace MWRender +{ + enum BlendMask + { + BlendMask_LowerBody = 1 << 0, + BlendMask_Torso = 1 << 1, + BlendMask_LeftArm = 1 << 2, + BlendMask_RightArm = 1 << 3, + + BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm, + + BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody + }; + /* This is the number of *discrete* blend masks. */ + static constexpr size_t sNumBlendMasks = 4; +} +#endif diff --git a/apps/openmw/mwrender/bonegroup.hpp b/apps/openmw/mwrender/bonegroup.hpp new file mode 100644 index 0000000000..2afedade86 --- /dev/null +++ b/apps/openmw/mwrender/bonegroup.hpp @@ -0,0 +1,16 @@ +#ifndef GAME_RENDER_BONEGROUP_H +#define GAME_RENDER_BONEGROUP_H + +namespace MWRender +{ + enum BoneGroup + { + BoneGroup_LowerBody = 0, + BoneGroup_Torso, + BoneGroup_LeftArm, + BoneGroup_RightArm, + + Num_BoneGroups + }; +} +#endif diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 88ceeabd23..aa6b5eb4dd 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -436,7 +436,7 @@ namespace MWRender // We still should use one-handed animation as fallback if (mAnimation->hasAnimation(inventoryGroup)) - groupname = inventoryGroup; + groupname = std::move(inventoryGroup); else { static const std::string oneHandFallback @@ -456,15 +456,15 @@ namespace MWRender mAnimation->showCarriedLeft(showCarriedLeft); - mCurrentAnimGroup = groupname; - mAnimation->play(mCurrentAnimGroup, 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); + mCurrentAnimGroup = std::move(groupname); + mAnimation->play(mCurrentAnimGroup, 1, BlendMask::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (torch != inv.end() && torch->getType() == ESM::Light::sRecordId && showCarriedLeft) { if (!mAnimation->getInfo("torch")) mAnimation->play( - "torch", 2, Animation::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, ~0ul, true); + "torch", 2, BlendMask::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, ~0ul, true); } else if (mAnimation->getInfo("torch")) mAnimation->disable("torch"); @@ -591,7 +591,7 @@ namespace MWRender void RaceSelectionPreview::onSetup() { CharacterPreview::onSetup(); - mAnimation->play("idle", 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); + mAnimation->play("idle", 1, BlendMask::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); mAnimation->runAnimation(0.f); // attach camera to follow the head node diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 040ba320a1..58e03aa0a2 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -170,7 +170,7 @@ namespace MWRender else source = mAnimationTimePtr[0]; - SceneUtil::AssignControllerSourcesVisitor assignVisitor(source); + SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::move(source)); attached->accept(assignVisitor); } catch (std::exception& e) diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index ac7a8a9351..f9dae65c40 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -363,7 +363,7 @@ namespace MWRender imageDest.mImage = image; imageDest.mX = x; imageDest.mY = y; - mPendingImageDest[camera] = imageDest; + mPendingImageDest[camera] = std::move(imageDest); } // Create a quad rendering the updated texture @@ -422,7 +422,8 @@ namespace MWRender if (cellX > mMaxX || cellX < mMinX || cellY > mMaxY || cellY < mMinY) return; - requestOverlayTextureUpdate(originX, mHeight - originY, cellSize, cellSize, localMapTexture, false, true); + requestOverlayTextureUpdate( + originX, mHeight - originY, cellSize, cellSize, std::move(localMapTexture), false, true); } void GlobalMap::clear() @@ -554,7 +555,7 @@ namespace MWRender { mOverlayImage = image; - requestOverlayTextureUpdate(0, 0, mWidth, mHeight, texture, true, false); + requestOverlayTextureUpdate(0, 0, mWidth, mHeight, std::move(texture), true, false); } else { @@ -562,7 +563,7 @@ namespace MWRender // In the latter case, we'll want filtering. // Create a RTT Camera and draw the image onto mOverlayImage in the next frame. requestOverlayTextureUpdate(destBox.mLeft, destBox.mTop, destBox.mRight - destBox.mLeft, - destBox.mBottom - destBox.mTop, texture, true, true, srcBox.mLeft / float(imageWidth), + destBox.mBottom - destBox.mTop, std::move(texture), true, true, srcBox.mLeft / float(imageWidth), srcBox.mTop / float(imageHeight), srcBox.mRight / float(imageWidth), srcBox.mBottom / float(imageHeight)); } diff --git a/apps/openmw/mwrender/luminancecalculator.cpp b/apps/openmw/mwrender/luminancecalculator.cpp index ae29b7fdcc..30918db87c 100644 --- a/apps/openmw/mwrender/luminancecalculator.cpp +++ b/apps/openmw/mwrender/luminancecalculator.cpp @@ -19,7 +19,7 @@ namespace MWRender auto resolveFragment = shaderManager.getShader("luminance/resolve.frag", defines); mResolveProgram = shaderManager.getProgram(vertex, std::move(resolveFragment)); - mLuminanceProgram = shaderManager.getProgram(vertex, std::move(luminanceFragment)); + mLuminanceProgram = shaderManager.getProgram(std::move(vertex), std::move(luminanceFragment)); for (auto& buffer : mBuffers) { diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 84522ee86e..b9ea257c9f 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -853,7 +853,7 @@ namespace MWRender src = mWeaponAnimationTime; else src = mAnimationTimePtr[0]; - SceneUtil::AssignControllerSourcesVisitor assignVisitor(src); + SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::move(src)); node->accept(assignVisitor); } } diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index 89e192f6c8..d93dc47641 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -68,7 +68,7 @@ namespace MWRender ptr.getClass().adjustScale(ptr, scaleVec, true); insert->setScale(scaleVec); - ptr.getRefData().setBaseNode(insert); + ptr.getRefData().setBaseNode(std::move(insert)); } void Objects::insertModel(const MWWorld::Ptr& ptr, const std::string& mesh, bool allowLight) diff --git a/apps/openmw/mwrender/pingpongcanvas.cpp b/apps/openmw/mwrender/pingpongcanvas.cpp index 9c8b08adfd..3af937045f 100644 --- a/apps/openmw/mwrender/pingpongcanvas.cpp +++ b/apps/openmw/mwrender/pingpongcanvas.cpp @@ -288,6 +288,8 @@ namespace MWRender pass.mRenderTarget->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0) .getTexture())); + assert(texture != nullptr); + texture->setTextureSize(w, h); texture->setNumMipmapLevels(pass.mRenderTexture->getNumMipmapLevels()); texture->dirtyTextureObject(); diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index 1aaeb460b7..42b2e4e1ee 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -662,6 +663,11 @@ namespace MWRender for (const auto& name : pass->getRenderTargets()) { + if (name.empty()) + { + continue; + } + auto& renderTarget = technique->getRenderTargetsMap()[name]; subPass.mStateSet->setTextureAttribute(subTexUnit, renderTarget.mTarget); subPass.mStateSet->addUniform(new osg::Uniform(name.c_str(), subTexUnit)); diff --git a/apps/openmw/mwrender/precipitationocclusion.cpp b/apps/openmw/mwrender/precipitationocclusion.cpp index 204b5c07bd..31712410b8 100644 --- a/apps/openmw/mwrender/precipitationocclusion.cpp +++ b/apps/openmw/mwrender/precipitationocclusion.cpp @@ -1,5 +1,7 @@ #include "precipitationocclusion.hpp" +#include + #include #include @@ -120,16 +122,19 @@ namespace MWRender void PrecipitationOccluder::update() { + if (!mRange.has_value()) + return; + const osg::Vec3 pos = mSceneCamera->getInverseViewMatrix().getTrans(); - const float zmin = pos.z() - mRange.z() - Constants::CellSizeInUnits; - const float zmax = pos.z() + mRange.z() + Constants::CellSizeInUnits; + const float zmin = pos.z() - mRange->z() - Constants::CellSizeInUnits; + const float zmax = pos.z() + mRange->z() + Constants::CellSizeInUnits; const float near = 0; const float far = zmax - zmin; - const float left = -mRange.x() / 2; + const float left = -mRange->x() / 2; const float right = -left; - const float top = mRange.y() / 2; + const float top = mRange->y() / 2; const float bottom = -top; if (SceneUtil::AutoDepth::isReversed()) @@ -163,10 +168,14 @@ namespace MWRender mSkyCullCallback = nullptr; mRootNode->removeChild(mCamera); + mRange = std::nullopt; } void PrecipitationOccluder::updateRange(const osg::Vec3f range) { + assert(range.x() != 0); + assert(range.y() != 0); + assert(range.z() != 0); const osg::Vec3f margin = { -50, -50, 0 }; mRange = range - margin; } diff --git a/apps/openmw/mwrender/precipitationocclusion.hpp b/apps/openmw/mwrender/precipitationocclusion.hpp index 26114ed42f..9d2992637e 100644 --- a/apps/openmw/mwrender/precipitationocclusion.hpp +++ b/apps/openmw/mwrender/precipitationocclusion.hpp @@ -4,6 +4,8 @@ #include #include +#include + namespace MWRender { class PrecipitationOccluder @@ -27,7 +29,7 @@ namespace MWRender osg::ref_ptr mCamera; osg::ref_ptr mSceneCamera; osg::ref_ptr mDepthTexture; - osg::Vec3f mRange; + std::optional mRange; }; } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index fa92fa1420..c2c6abd1bc 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -716,9 +716,12 @@ namespace MWRender // need to wrap this in a StateUpdater? mSunLight->setPosition(osg::Vec4(position.x(), position.y(), position.z(), 0)); + // The sun is not synchronized with the sunlight because sunlight origin can't reach the horizon + // This is based on exterior sun orbit and won't make sense for interiors, see WeatherManager::update + position.z() = 400.f - std::abs(position.x()); mSky->setSunDirection(position); - mPostProcessor->getStateUpdater()->setSunPos(mSunLight->getPosition(), mNight); + mPostProcessor->getStateUpdater()->setSunPos(osg::Vec4f(position, 0.f), mNight); } void RenderingManager::addCell(const MWWorld::CellStore* store) diff --git a/apps/openmw/mwrender/ripples.cpp b/apps/openmw/mwrender/ripples.cpp index 130e005729..dea372666e 100644 --- a/apps/openmw/mwrender/ripples.cpp +++ b/apps/openmw/mwrender/ripples.cpp @@ -106,7 +106,7 @@ namespace MWRender mProgramBlobber = shaderManager.getProgram( vertex, shaderManager.getShader("ripples_blobber.frag", defineMap, osg::Shader::FRAGMENT)); mProgramSimulation = shaderManager.getProgram( - vertex, shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT)); + std::move(vertex), shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT)); } void RipplesSurface::setupComputePipeline() diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 6df3734252..57c3e902d8 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -306,7 +306,7 @@ namespace MWRender bool forceShaders = mSceneManager->getForceShaders(); - mAtmosphereDay = mSceneManager->getInstance(Settings::models().mSkyatmosphere, mEarlyRenderBinRoot); + mAtmosphereDay = mSceneManager->getInstance(Settings::models().mSkyatmosphere.get(), mEarlyRenderBinRoot); ModVertexAlphaVisitor modAtmosphere(ModVertexAlphaVisitor::Atmosphere); mAtmosphereDay->accept(modAtmosphere); @@ -319,9 +319,9 @@ namespace MWRender osg::ref_ptr atmosphereNight; if (mSceneManager->getVFS()->exists(Settings::models().mSkynight02.get())) - atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight02, mAtmosphereNightNode); + atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight02.get(), mAtmosphereNightNode); else - atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight01, mAtmosphereNightNode); + atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight01.get(), mAtmosphereNightNode); atmosphereNight->getOrCreateStateSet()->setAttributeAndModes( createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); @@ -341,7 +341,8 @@ namespace MWRender mEarlyRenderBinRoot->addChild(mCloudNode); mCloudMesh = new osg::PositionAttitudeTransform; - osg::ref_ptr cloudMeshChild = mSceneManager->getInstance(Settings::models().mSkyclouds, mCloudMesh); + osg::ref_ptr cloudMeshChild + = mSceneManager->getInstance(Settings::models().mSkyclouds.get(), mCloudMesh); mCloudUpdater = new CloudUpdater(forceShaders); mCloudUpdater->setOpacity(1.f); cloudMeshChild->addUpdateCallback(mCloudUpdater); @@ -349,7 +350,7 @@ namespace MWRender mNextCloudMesh = new osg::PositionAttitudeTransform; osg::ref_ptr nextCloudMeshChild - = mSceneManager->getInstance(Settings::models().mSkyclouds, mNextCloudMesh); + = mSceneManager->getInstance(Settings::models().mSkyclouds.get(), mNextCloudMesh); mNextCloudUpdater = new CloudUpdater(forceShaders); mNextCloudUpdater->setOpacity(0.f); nextCloudMeshChild->addUpdateCallback(mNextCloudUpdater); @@ -764,7 +765,7 @@ namespace MWRender cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - mCloudUpdater->setTexture(cloudTex); + mCloudUpdater->setTexture(std::move(cloudTex)); } if (mStormDirection != weather.mStormDirection) @@ -786,7 +787,7 @@ namespace MWRender cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - mNextCloudUpdater->setTexture(cloudTex); + mNextCloudUpdater->setTexture(std::move(cloudTex)); mNextStormDirection = weather.mStormDirection; } } diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 553bdeeaaa..d5fb01242f 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -722,8 +722,8 @@ namespace MWRender mRainSettingsUpdater = new RainSettingsUpdater(); node->setUpdateCallback(mRainSettingsUpdater); - mShaderWaterStateSetUpdater - = new ShaderWaterStateSetUpdater(this, mReflection, mRefraction, mRipples, std::move(program), normalMap); + mShaderWaterStateSetUpdater = new ShaderWaterStateSetUpdater( + this, mReflection, mRefraction, mRipples, std::move(program), std::move(normalMap)); node->addCullCallback(mShaderWaterStateSetUpdater); } diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 9d75334f00..476cacafd0 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -1473,7 +1473,7 @@ namespace MWScript if (lastTextureSrc.empty() || textureSrc != lastTextureSrc) { - lastTextureSrc = textureSrc; + lastTextureSrc = std::move(textureSrc); if (lastTextureSrc.empty()) lastTextureSrc = "[No Source]"; diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index d617a02b9a..4bc59e1524 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -85,7 +85,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).getLevel(); + Interpreter::Type_Integer value = -1; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getLevel(); runtime.push(value); } @@ -102,7 +104,8 @@ namespace MWScript Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats(ptr).setLevel(value); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).setLevel(value); } }; @@ -121,7 +124,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float value = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).getModified(); + Interpreter::Type_Float value = 0.f; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).getModified(); runtime.push(value); } @@ -145,6 +150,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); attribute.setBase(value, true); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); @@ -169,6 +177,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); modStat(attribute, value); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); @@ -189,14 +200,14 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float value; + Interpreter::Type_Float value = 0.f; if (mIndex == 0 && ptr.getClass().hasItemHealth(ptr)) { // health is a special case value = static_cast(ptr.getClass().getItemMaxHealth(ptr)); } - else + else if (ptr.getClass().isActor()) { value = ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex).getCurrent(); // GetMagicka shouldn't return negative values @@ -225,6 +236,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::DynamicStat stat(ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex)); stat.setBase(value); @@ -254,6 +268,9 @@ namespace MWScript Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + // workaround broken endgame scripts that kill dagoth ur if (!R::implicit && ptr.getCellRef().getRefId() == "dagoth_ur_1") { @@ -301,6 +318,9 @@ namespace MWScript Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); @@ -336,6 +356,13 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); + + if (!ptr.getClass().isActor()) + { + runtime.push(0.f); + return; + } + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); runtime.push(stats.getDynamic(mIndex).getRatio()); @@ -357,6 +384,12 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + { + runtime.push(0.f); + return; + } + Interpreter::Type_Float value = ptr.getClass().getSkill(ptr, mId); runtime.push(value); @@ -381,6 +414,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isNpc()) + return; + MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats(ptr); stats.getSkill(mId).setBase(value, true); @@ -405,6 +441,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isNpc()) + return; + MWMechanics::SkillValue& skill = ptr.getClass().getNpcStats(ptr).getSkill(mId); modStat(skill, value); } @@ -465,6 +504,9 @@ namespace MWScript ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + if (!ptr.getClass().isActor()) + return; + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(id); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); @@ -491,6 +533,9 @@ namespace MWScript ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); creatureStats.getSpells().remove(id); @@ -514,7 +559,8 @@ namespace MWScript ESM::RefId spellid = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffects(ptr, spellid); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffects(ptr, spellid); } }; @@ -529,7 +575,8 @@ namespace MWScript Interpreter::Type_Integer effectId = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(ptr, effectId); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(ptr, effectId); } }; @@ -845,7 +892,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats(ptr).hasCommonDisease()); + if (ptr.getClass().isActor()) + runtime.push(ptr.getClass().getCreatureStats(ptr).hasCommonDisease()); + else + runtime.push(0); } }; @@ -857,7 +907,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats(ptr).hasBlightDisease()); + if (ptr.getClass().isActor()) + runtime.push(ptr.getClass().getCreatureStats(ptr).hasBlightDisease()); + else + runtime.push(0); } }; @@ -872,9 +925,16 @@ namespace MWScript ESM::RefId race = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - const ESM::RefId& npcRace = ptr.get()->mBase->mRace; + if (ptr.getClass().isNpc()) + { + const ESM::RefId& npcRace = ptr.get()->mBase->mRace; - runtime.push(race == npcRace); + runtime.push(race == npcRace); + } + else + { + runtime.push(0); + } } }; @@ -1043,10 +1103,15 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).hasDied(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + value = stats.hasDied(); - if (value) - ptr.getClass().getCreatureStats(ptr).clearHasDied(); + if (value) + stats.clearHasDied(); + } runtime.push(value); } @@ -1060,10 +1125,15 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).hasBeenMurdered(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + value = stats.hasBeenMurdered(); - if (value) - ptr.getClass().getCreatureStats(ptr).clearHasBeenMurdered(); + if (value) + stats.clearHasBeenMurdered(); + } runtime.push(value); } @@ -1077,7 +1147,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).getKnockedDownOneFrame(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getKnockedDownOneFrame(); runtime.push(value); } @@ -1090,7 +1162,10 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getNpcStats(ptr).isWerewolf()); + if (ptr.getClass().isNpc()) + runtime.push(ptr.getClass().getNpcStats(ptr).isWerewolf()); + else + runtime.push(0); } }; @@ -1101,7 +1176,8 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, set); + if (ptr.getClass().isNpc()) + MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, set); } }; @@ -1112,7 +1188,8 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr); + if (ptr.getClass().isNpc()) + MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr); } }; @@ -1124,6 +1201,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + return; + if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); @@ -1192,6 +1272,12 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + { + runtime.push(0); + return; + } + const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) @@ -1226,6 +1312,13 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); + + int arg = runtime[0].mInteger; + runtime.pop(); + + if (!ptr.getClass().isActor()) + return; + MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) @@ -1239,8 +1332,6 @@ namespace MWScript if (mPositiveEffect == ESM::MagicEffect::ResistFrost) currentValue += effects.getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude(); - int arg = runtime[0].mInteger; - runtime.pop(); effects.modifyBase(mPositiveEffect, (arg - static_cast(currentValue))); } }; @@ -1261,10 +1352,14 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); int arg = runtime[0].mInteger; runtime.pop(); + + if (!ptr.getClass().isActor()) + return; + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); stats.getMagicEffects().modifyBase(mPositiveEffect, arg); } }; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 64f8959218..0cc276807f 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -222,7 +223,7 @@ namespace MWSound params.mFlags = PlayMode::NoEnv | Type::Voice | Play_2D; return params; }()); - played = mOutput->streamSound(decoder, sound.get(), true); + played = mOutput->streamSound(std::move(decoder), sound.get(), true); } else { @@ -235,7 +236,7 @@ namespace MWSound params.mFlags = PlayMode::Normal | Type::Voice | Play_3D; return params; }()); - played = mOutput->streamSound3D(decoder, sound.get(), true); + played = mOutput->streamSound3D(std::move(decoder), sound.get(), true); } if (!played) return nullptr; @@ -281,7 +282,7 @@ namespace MWSound params.mFlags = PlayMode::NoEnvNoScaling | Type::Music | Play_2D; return params; }()); - mOutput->streamSound(decoder, mMusic.get()); + mOutput->streamSound(std::move(decoder), mMusic.get()); } void SoundManager::advanceMusic(const std::string& filename, float fadeOut) @@ -365,7 +366,7 @@ namespace MWSound for (const auto& name : mVFS->getRecursiveDirectoryIterator(playlistPath)) filelist.push_back(name); - mMusicFiles[playlist] = filelist; + mMusicFiles[playlist] = std::move(filelist); } // No Battle music? Use Explore playlist @@ -392,7 +393,7 @@ namespace MWSound const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans(); stopSay(ptr); - StreamPtr sound = playVoice(decoder, pos, (ptr == MWMechanics::getPlayer())); + StreamPtr sound = playVoice(std::move(decoder), pos, (ptr == MWMechanics::getPlayer())); if (!sound) return; @@ -421,7 +422,7 @@ namespace MWSound return; stopSay(MWWorld::ConstPtr()); - StreamPtr sound = playVoice(decoder, osg::Vec3f(), true); + StreamPtr sound = playVoice(std::move(decoder), osg::Vec3f(), true); if (!sound) return; diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index e80debb998..9f0f8d2928 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -81,10 +82,8 @@ std::map MWState::StateManager::buildContentFileIndexMap(const ESM::ES for (int iPrev = 0; iPrev < static_cast(prev.size()); ++iPrev) { - std::string id = Misc::StringUtils::lowerCase(prev[iPrev].name); - for (int iCurrent = 0; iCurrent < static_cast(current.size()); ++iCurrent) - if (id == Misc::StringUtils::lowerCase(current[iCurrent])) + if (Misc::StringUtils::ciEqual(prev[iPrev].name, current[iCurrent])) { map.insert(std::make_pair(iPrev, iCurrent)); break; @@ -412,9 +411,38 @@ void MWState::StateManager::loadGame(const std::filesystem::path& filepath) loadGame(character, filepath); } -struct VersionMismatchError : public std::runtime_error +struct SaveFormatVersionError : public std::exception { - using std::runtime_error::runtime_error; + using std::exception::exception; + + SaveFormatVersionError(ESM::FormatVersion savegameFormat, const std::string& message) + : mSavegameFormat(savegameFormat) + , mErrorMessage(message) + { + } + + const char* what() const noexcept override { return mErrorMessage.c_str(); } + ESM::FormatVersion getFormatVersion() const { return mSavegameFormat; } + +protected: + ESM::FormatVersion mSavegameFormat = ESM::DefaultFormatVersion; + std::string mErrorMessage; +}; + +struct SaveVersionTooOldError : SaveFormatVersionError +{ + SaveVersionTooOldError(ESM::FormatVersion savegameFormat) + : SaveFormatVersionError(savegameFormat, "format version " + std::to_string(savegameFormat) + " is too old") + { + } +}; + +struct SaveVersionTooNewError : SaveFormatVersionError +{ + SaveVersionTooNewError(ESM::FormatVersion savegameFormat) + : SaveFormatVersionError(savegameFormat, "format version " + std::to_string(savegameFormat) + " is too new") + { + } }; void MWState::StateManager::loadGame(const Character* character, const std::filesystem::path& filepath) @@ -434,23 +462,9 @@ void MWState::StateManager::loadGame(const Character* character, const std::file ESM::FormatVersion version = reader.getFormatVersion(); if (version > ESM::CurrentSaveGameFormatVersion) - throw VersionMismatchError("#{OMWEngine:LoadingRequiresNewVersionError}"); + throw SaveVersionTooNewError(version); else if (version < ESM::MinSupportedSaveGameFormatVersion) - { - const char* release; - // Report the last version still capable of reading this save - if (version <= ESM::OpenMW0_48SaveGameFormatVersion) - release = "OpenMW 0.48.0"; - else - { - // Insert additional else if statements above to cover future releases - static_assert(ESM::MinSupportedSaveGameFormatVersion <= ESM::OpenMW0_49SaveGameFormatVersion); - release = "OpenMW 0.49.0"; - } - auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); - std::string message = l10n->formatMessage("LoadingRequiresOldVersionError", { "version" }, { release }); - throw VersionMismatchError(message); - } + throw SaveVersionTooOldError(version); std::map contentFileMap = buildContentFileIndexMap(reader); reader.setContentFileMapping(&contentFileMap); @@ -632,23 +646,49 @@ void MWState::StateManager::loadGame(const Character* character, const std::file MWBase::Environment::get().getLuaManager()->gameLoaded(); } + catch (const SaveVersionTooNewError& e) + { + std::string error = "#{OMWEngine:LoadingRequiresNewVersionError}"; + printSavegameFormatError(e.what(), error); + } + catch (const SaveVersionTooOldError& e) + { + const char* release; + // Report the last version still capable of reading this save + if (e.getFormatVersion() <= ESM::OpenMW0_48SaveGameFormatVersion) + release = "OpenMW 0.48.0"; + else + { + // Insert additional else if statements above to cover future releases + static_assert(ESM::MinSupportedSaveGameFormatVersion <= ESM::OpenMW0_49SaveGameFormatVersion); + release = "OpenMW 0.49.0"; + } + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); + std::string error = l10n->formatMessage("LoadingRequiresOldVersionError", { "version" }, { release }); + printSavegameFormatError(e.what(), error); + } catch (const std::exception& e) { - Log(Debug::Error) << "Failed to load saved game: " << e.what(); - - cleanup(true); - - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); - - std::vector buttons; - buttons.emplace_back("#{Interface:OK}"); - std::string error = "#{OMWEngine:LoadingFailed}: " + std::string(e.what()); - - MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error, buttons); + printSavegameFormatError(e.what(), error); } } +void MWState::StateManager::printSavegameFormatError( + const std::string& exceptionText, const std::string& messageBoxText) +{ + Log(Debug::Error) << "Failed to load saved game: " << exceptionText; + + cleanup(true); + + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); + + std::vector buttons; + buttons.emplace_back("#{Interface:OK}"); + + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(messageBoxText, buttons); +} + void MWState::StateManager::quickLoad() { if (Character* currentCharacter = getCurrentCharacter()) diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index d632d11137..b08584a817 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -24,6 +24,8 @@ namespace MWState private: void cleanup(bool force = false); + void printSavegameFormatError(const std::string& exceptionText, const std::string& messageBoxText); + bool confirmLoading(const std::vector& missingFiles) const; void writeScreenshot(std::vector& imageData) const; diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 7da5e8f848..fe14856364 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -52,20 +52,13 @@ namespace MWWorld struct ListModelsVisitor { - ListModelsVisitor(std::vector& out) - : mOut(out) - { - } - - virtual bool operator()(const MWWorld::Ptr& ptr) + bool operator()(const MWWorld::ConstPtr& ptr) { ptr.getClass().getModelsToPreload(ptr, mOut); return true; } - virtual ~ListModelsVisitor() = default; - std::vector& mOut; }; @@ -90,8 +83,8 @@ namespace MWWorld { mTerrainView = mTerrain->createView(); - ListModelsVisitor visitor(mMeshes); - cell->forEach(visitor); + ListModelsVisitor visitor{ mMeshes }; + cell->forEachConst(visitor); } void abort() override { mAbort = true; } diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index 0c6527ce22..0a78746479 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -209,8 +209,8 @@ namespace MWWorld /// false will abort the iteration. /// \note Prefer using forEachConst when possible. /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in - /// unintended behaviour. \attention This function also lists deleted (count 0) objects! \return Iteration - /// completed? + /// unintended behaviour. \attention This function also lists deleted (count 0) objects! + /// \return Iteration completed? template bool forEach(Visitor&& visitor) { @@ -224,12 +224,12 @@ namespace MWWorld mHasState = true; - for (unsigned int i = 0; i < mMergedRefs.size(); ++i) + for (LiveCellRefBase* mergedRef : mMergedRefs) { - if (!isAccessible(mMergedRefs[i]->mData, mMergedRefs[i]->mRef)) + if (!isAccessible(mergedRef->mData, mergedRef->mRef)) continue; - if (!visitor(MWWorld::Ptr(mMergedRefs[i], this))) + if (!visitor(MWWorld::Ptr(mergedRef, this))) return false; } return true; @@ -238,8 +238,8 @@ namespace MWWorld /// Call visitor (MWWorld::ConstPtr) for each reference. visitor must return a bool. Returning /// false will abort the iteration. /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in - /// unintended behaviour. \attention This function also lists deleted (count 0) objects! \return Iteration - /// completed? + /// unintended behaviour. \attention This function also lists deleted (count 0) objects! + /// \return Iteration completed? template bool forEachConst(Visitor&& visitor) const { @@ -249,12 +249,12 @@ namespace MWWorld if (mMergedRefsNeedsUpdate) updateMergedRefs(); - for (unsigned int i = 0; i < mMergedRefs.size(); ++i) + for (const LiveCellRefBase* mergedRef : mMergedRefs) { - if (!isAccessible(mMergedRefs[i]->mData, mMergedRefs[i]->mRef)) + if (!isAccessible(mergedRef->mData, mergedRef->mRef)) continue; - if (!visitor(MWWorld::ConstPtr(mMergedRefs[i], this))) + if (!visitor(MWWorld::ConstPtr(mergedRef, this))) return false; } return true; @@ -263,8 +263,8 @@ namespace MWWorld /// Call visitor (ref) for each reference of given type. visitor must return a bool. Returning /// false will abort the iteration. /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in - /// unintended behaviour. \attention This function also lists deleted (count 0) objects! \return Iteration - /// completed? + /// unintended behaviour. \attention This function also lists deleted (count 0) objects! + /// \return Iteration completed? template bool forEachType(Visitor&& visitor) { diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index d5062d6add..82aa83e7c6 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -118,11 +118,6 @@ namespace MWWorld throw std::runtime_error("class cannot hit"); } - void Class::block(const Ptr& ptr) const - { - throw std::runtime_error("class cannot block"); - } - void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const { @@ -149,7 +144,7 @@ namespace MWWorld throw std::runtime_error("class does not have an inventory store"); } - bool Class::hasInventoryStore(const Ptr& ptr) const + bool Class::hasInventoryStore(const ConstPtr& ptr) const { return false; } @@ -320,7 +315,7 @@ namespace MWWorld return false; } - void Class::getModelsToPreload(const Ptr& ptr, std::vector& models) const + void Class::getModelsToPreload(const ConstPtr& ptr, std::vector& models) const { std::string model = getModel(ptr); if (!model.empty()) diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 87e70b3198..c787505238 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -151,10 +151,6 @@ namespace MWWorld /// actor responsible for the attack. \a successful specifies if the hit is /// successful or not. \a sourceType classifies the damage source. - virtual void block(const Ptr& ptr) const; - ///< Play the appropriate sound for a blocked attack, depending on the currently equipped shield - /// (default implementation: throw an exception) - virtual std::unique_ptr activate(const Ptr& ptr, const Ptr& actor) const; ///< Generate action for activation (default implementation: return a null action). @@ -170,7 +166,7 @@ namespace MWWorld ///< Return inventory store or throw an exception, if class does not have a /// inventory store (default implementation: throw an exception) - virtual bool hasInventoryStore(const Ptr& ptr) const; + virtual bool hasInventoryStore(const ConstPtr& ptr) const; ///< Does this object have an inventory store, i.e. equipment slots? (default implementation: false) virtual bool canLock(const ConstPtr& ptr) const; @@ -284,7 +280,7 @@ namespace MWWorld virtual bool useAnim() const; ///< Whether or not to use animated variant of model (default false) - virtual void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const; + virtual void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: ///< list getModel(). diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index d30ea21494..f48f73f48a 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -719,7 +719,7 @@ MWWorld::ResolutionHandle MWWorld::ContainerStore::resolveTemporarily() fill(container.get()->mBase->mInventory, ESM::RefId(), prng); addScripts(*this, container.mCell); } - return { listener }; + return { std::move(listener) }; } void MWWorld::ContainerStore::unresolve() diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index ac3ee72a94..10d9fb3f3b 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -893,7 +893,7 @@ namespace MWWorld // Try to overwrite existing record auto ret = mStatic.emplace(cell, pathgrid); if (!ret.second) - ret.first->second = pathgrid; + ret.first->second = std::move(pathgrid); return RecordId(ESM::RefId(), isDeleted); } diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 2c9b80bc5f..36b5958dc3 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -180,7 +180,6 @@ namespace MWWorld , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) - , mThunderSoundID() , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) , mFlashBrightness(0.0f) { @@ -748,21 +747,21 @@ namespace MWWorld const float dayDuration = adjustedNightStart - mSunriseTime; const float nightDuration = 24.f - dayDuration; - double theta; + float orbit; if (!is_night) { - theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; + float t = (adjustedHour - mSunriseTime) / dayDuration; + orbit = 1.f - 2.f * t; } else { - theta = static_cast(osg::PI) - - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; + float t = (adjustedHour - adjustedNightStart) / nightDuration; + orbit = 2.f * t - 1.f; } - osg::Vec3f final(static_cast(cos(theta)), - -0.268f, // approx tan( -15 degrees ) - static_cast(sin(theta))); - mRendering.setSunDirection(final * -1); + // Hardcoded constant from Morrowind + const osg::Vec3f sunDir(-400.f * orbit, 75.f, -100.f); + mRendering.setSunDirection(sunDir); mRendering.setNight(is_night); } @@ -823,19 +822,29 @@ namespace MWWorld void WeatherManager::stopSounds() { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); if (mAmbientSound) { - MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); + sndMgr->stopSound(mAmbientSound); mAmbientSound = nullptr; } mPlayingAmbientSoundID = ESM::RefId(); if (mRainSound) { - MWBase::Environment::get().getSoundManager()->stopSound(mRainSound); + sndMgr->stopSound(mRainSound); mRainSound = nullptr; } mPlayingRainSoundID = ESM::RefId(); + + for (ESM::RefId soundId : mWeatherSettings[mCurrentWeather].mThunderSoundID) + if (!soundId.empty() && sndMgr->getSoundPlaying(MWWorld::ConstPtr(), soundId)) + sndMgr->stopSound3D(MWWorld::ConstPtr(), soundId); + + if (inTransition()) + for (ESM::RefId soundId : mWeatherSettings[mNextWeather].mThunderSoundID) + if (!soundId.empty() && sndMgr->getSoundPlaying(MWWorld::ConstPtr(), soundId)) + sndMgr->stopSound3D(MWWorld::ConstPtr(), soundId); } float WeatherManager::getWindSpeed() const diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index 327136a859..0643240dcd 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -162,6 +162,8 @@ namespace MWWorld // This is used for Rain and Thunderstorm ESM::RefId mRainLoopSoundID; + std::array mThunderSoundID; + // Is this an ash storm / blight storm? If so, the following will happen: // - The particles and clouds will be oriented so they appear to come from the Red Mountain. // - Characters will animate their hand to protect eyes from the storm when looking in its direction (idlestorm @@ -213,7 +215,6 @@ namespace MWWorld // non-zero values. float mThunderFrequency; float mThunderThreshold; - ESM::RefId mThunderSoundID[4]; float mFlashDecrement; float mFlashBrightness; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 1b6af6038e..f20cbd208f 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -60,6 +60,7 @@ #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" @@ -997,6 +998,9 @@ namespace MWWorld { MWWorld::Ptr facedObject; + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + return facedObject; + if (MWBase::Environment::get().getWindowManager()->isGuiMode() && MWBase::Environment::get().getWindowManager()->isConsoleMode()) facedObject = getFacedObject(getMaxActivationDistance() * 50, false); diff --git a/apps/openmw_test_suite/detournavigator/gettilespositions.cpp b/apps/openmw_test_suite/detournavigator/gettilespositions.cpp index fb32022010..729d11ddb5 100644 --- a/apps/openmw_test_suite/detournavigator/gettilespositions.cpp +++ b/apps/openmw_test_suite/detournavigator/gettilespositions.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -86,4 +87,79 @@ namespace EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0))); } + + struct TilesPositionsRangeParams + { + TilesPositionsRange mA; + TilesPositionsRange mB; + TilesPositionsRange mResult; + }; + + struct DetourNavigatorGetIntersectionTest : TestWithParam + { + }; + + TEST_P(DetourNavigatorGetIntersectionTest, should_return_expected_result) + { + EXPECT_EQ(getIntersection(GetParam().mA, GetParam().mB), GetParam().mResult); + EXPECT_EQ(getIntersection(GetParam().mB, GetParam().mA), GetParam().mResult); + } + + const TilesPositionsRangeParams getIntersectionParams[] = { + { .mA = TilesPositionsRange{}, .mB = TilesPositionsRange{}, .mResult = TilesPositionsRange{} }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 2, 2 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 2, 2 } }, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 2, 2 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{}, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 2, 2 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 1, 1 } }, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 0, 2 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{}, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 2, 0 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{}, + }, + }; + + INSTANTIATE_TEST_SUITE_P( + GetIntersectionParams, DetourNavigatorGetIntersectionTest, ValuesIn(getIntersectionParams)); + + struct DetourNavigatorGetUnionTest : TestWithParam + { + }; + + TEST_P(DetourNavigatorGetUnionTest, should_return_expected_result) + { + EXPECT_EQ(getUnion(GetParam().mA, GetParam().mB), GetParam().mResult); + EXPECT_EQ(getUnion(GetParam().mB, GetParam().mA), GetParam().mResult); + } + + const TilesPositionsRangeParams getUnionParams[] = { + { .mA = TilesPositionsRange{}, .mB = TilesPositionsRange{}, .mResult = TilesPositionsRange{} }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 2, 2 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 3, 3 } }, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 2, 2 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 2, 2 } }, + }, + }; + + INSTANTIATE_TEST_SUITE_P(GetUnionParams, DetourNavigatorGetUnionTest, ValuesIn(getUnionParams)); } diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index df4d7a1e99..aba8598f18 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -39,6 +39,8 @@ namespace using namespace DetourNavigator; using namespace DetourNavigator::Tests; + constexpr int heightfieldTileSize = ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1); + struct DetourNavigatorNavigatorTest : Test { Settings mSettings = makeSettings(); @@ -53,7 +55,6 @@ namespace AreaCosts mAreaCosts; Loading::Listener mListener; const osg::Vec2i mCellPosition{ 0, 0 }; - const int mHeightfieldTileSize = ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1); const float mEndTolerance = 0; const btTransform mTransform{ btMatrix3x3::getIdentity(), btVector3(256, 256, 0) }; const ObjectTransform mObjectTransform{ ESM::Position{ { 256, 256, 0 }, { 0, 0, 0 } }, 0.0f }; @@ -129,7 +130,7 @@ namespace { } - T& shape() { return static_cast(*mInstance->mCollisionShape); } + T& shape() const { return static_cast(*mInstance->mCollisionShape); } const osg::ref_ptr& instance() const { return mInstance; } private: @@ -167,7 +168,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); auto updateGuard = mNavigator->makeUpdateGuard(); @@ -189,7 +190,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_to_the_start_position_should_contain_single_point) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); auto updateGuard = mNavigator->makeUpdateGuard(); @@ -211,7 +212,7 @@ namespace mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape( @@ -256,7 +257,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape( @@ -335,7 +336,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, only_one_heightfield_per_cell_is_allowed) { const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize1 = mHeightfieldTileSize * (surface1.mSize - 1); + const int cellSize1 = heightfieldTileSize * (surface1.mSize - 1); const std::array heightfieldData2{ { -25, -25, -25, -25, -25, // row 0 @@ -345,7 +346,7 @@ namespace -25, -25, -25, -25, -25, // row 4 } }; const HeightfieldSurface surface2 = makeSquareHeightfieldSurface(heightfieldData2); - const int cellSize2 = mHeightfieldTileSize * (surface2.mSize - 1); + const int cellSize2 = heightfieldTileSize * (surface2.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize1, surface1, nullptr); @@ -412,7 +413,7 @@ namespace 0, -50, -100, -100, -100, // row 4 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addWater(mCellPosition, cellSize, 300, nullptr); @@ -446,7 +447,7 @@ namespace 0, 0, 0, 0, 0, 0, 0, // row 6 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addWater(mCellPosition, cellSize, -25, nullptr); @@ -480,7 +481,7 @@ namespace 0, 0, 0, 0, 0, 0, 0, // row 6 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); @@ -513,7 +514,7 @@ namespace 0, 0, 0, 0, 0, 0, 0, // row 6 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addWater(mCellPosition, cellSize, -25, nullptr); @@ -566,7 +567,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_heightfield_remove_and_update_then_find_path_should_return_path) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); @@ -602,7 +603,7 @@ namespace 0, -25, -100, -100, -100, -100, // row 5 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); @@ -629,7 +630,7 @@ namespace mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); const btVector3 shift = getHeightfieldShift(mCellPosition, cellSize, surface.mMinHeight, surface.mMaxHeight); std::vector> boxes; @@ -718,7 +719,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_raycast_should_return_position) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); @@ -737,7 +738,7 @@ namespace update_for_oscillating_object_that_does_not_change_navmesh_should_not_trigger_navmesh_update) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance oscillatingBox(std::make_unique(btVector3(20, 20, 20))); const btVector3 oscillatingBoxShapePosition(288, 288, 400); @@ -777,7 +778,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, should_provide_path_over_flat_heightfield) { const HeightfieldPlane plane{ 100 }; - const int cellSize = mHeightfieldTileSize * 4; + const int cellSize = heightfieldTileSize * 4; ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, plane, nullptr); @@ -796,7 +797,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, for_not_reachable_destination_find_path_should_provide_partial_path) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), @@ -822,7 +823,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, end_tolerance_should_extent_available_destinations) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), @@ -948,7 +949,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nav_mesh_position) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); auto updateGuard = mNavigator->makeUpdateGuard(); @@ -966,7 +967,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_too_far) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); auto updateGuard = mNavigator->makeUpdateGuard(); @@ -984,7 +985,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_flags_do_not_match) { const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); auto updateGuard = mNavigator->makeUpdateGuard(); @@ -998,4 +999,142 @@ namespace EXPECT_EQ(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_swim), std::nullopt); } + + struct DetourNavigatorUpdateTest : TestWithParam> + { + }; + + std::vector getUsedTiles(const NavMeshCacheItem& navMesh) + { + std::vector result; + navMesh.forEachUsedTile([&](const TilePosition& position, const auto&...) { result.push_back(position); }); + return result; + } + + TEST_P(DetourNavigatorUpdateTest, update_should_change_covered_area_when_player_moves) + { + Loading::Listener listener; + Settings settings = makeSettings(); + settings.mMaxTilesNumber = 5; + NavigatorImpl navigator(settings, nullptr); + const AgentBounds agentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; + ASSERT_TRUE(navigator.addAgent(agentBounds)); + + GetParam()(navigator); + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(3000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::allJobsDone, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTiles[] = { { 3, 4 }, { 4, 3 }, { 4, 4 }, { 4, 5 }, { 5, 4 } }; + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, UnorderedElementsAreArray(expectedTiles)) << usedTiles; + } + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(4000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::allJobsDone, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTiles[] = { { 4, 4 }, { 5, 3 }, { 5, 4 }, { 5, 5 }, { 6, 4 } }; + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, UnorderedElementsAreArray(expectedTiles)) << usedTiles; + } + } + + struct AddHeightfieldSurface + { + static constexpr std::size_t sSize = 65; + static constexpr float sHeights[sSize * sSize]{}; + + void operator()(Navigator& navigator) const + { + const osg::Vec2i cellPosition(0, 0); + const HeightfieldSurface surface{ + .mHeights = sHeights, + .mSize = sSize, + .mMinHeight = -1, + .mMaxHeight = 1, + }; + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + navigator.addHeightfield(cellPosition, cellSize, surface, nullptr); + } + }; + + struct AddHeightfieldPlane + { + void operator()(Navigator& navigator) const + { + const osg::Vec2i cellPosition(0, 0); + const HeightfieldPlane plane{ .mHeight = 0 }; + const int cellSize = 8192; + navigator.addHeightfield(cellPosition, cellSize, plane, nullptr); + } + }; + + struct AddWater + { + void operator()(Navigator& navigator) const + { + const osg::Vec2i cellPosition(0, 0); + const float level = 0; + const int cellSize = 8192; + navigator.addWater(cellPosition, cellSize, level, nullptr); + } + }; + + struct AddObject + { + const float mSize = 8192; + CollisionShapeInstance mBox{ std::make_unique(btVector3(mSize, mSize, 1)) }; + const ObjectTransform mTransform{ + .mPosition = ESM::Position{ .pos = { 0, 0, 0 }, .rot{ 0, 0, 0 } }, + .mScale = 1.0f, + }; + + void operator()(Navigator& navigator) const + { + navigator.addObject(ObjectId(&mBox.shape()), ObjectShapes(mBox.instance(), mTransform), + btTransform::getIdentity(), nullptr); + } + }; + + struct AddAll + { + AddHeightfieldSurface mAddHeightfieldSurface; + AddHeightfieldPlane mAddHeightfieldPlane; + AddWater mAddWater; + AddObject mAddObject; + + void operator()(Navigator& navigator) const + { + mAddHeightfieldSurface(navigator); + mAddHeightfieldPlane(navigator); + mAddWater(navigator); + mAddObject(navigator); + } + }; + + const std::function addNavMeshData[] = { + AddHeightfieldSurface{}, + AddHeightfieldPlane{}, + AddWater{}, + AddObject{}, + AddAll{}, + }; + + INSTANTIATE_TEST_SUITE_P(DifferentNavMeshData, DetourNavigatorUpdateTest, ValuesIn(addNavMeshData)); } diff --git a/apps/openmw_test_suite/detournavigator/operators.hpp b/apps/openmw_test_suite/detournavigator/operators.hpp index 4e42af78e4..4c043027eb 100644 --- a/apps/openmw_test_suite/detournavigator/operators.hpp +++ b/apps/openmw_test_suite/detournavigator/operators.hpp @@ -42,12 +42,24 @@ namespace testing << ", " << value.y() << ", " << value.z() << ')'; } + template <> + inline testing::Message& Message::operator<<(const osg::Vec2i& value) + { + return (*this) << "{" << value.x() << ", " << value.y() << '}'; + } + template <> inline testing::Message& Message::operator<<(const Wrapper& value) { return (*this) << value.mValue; } + template <> + inline testing::Message& Message::operator<<(const Wrapper& value) + { + return (*this) << value.mValue; + } + template <> inline testing::Message& Message::operator<<(const Wrapper& value) { @@ -72,6 +84,12 @@ namespace testing return writeRange(*this, value, 1); } + template <> + inline testing::Message& Message::operator<<(const std::vector& value) + { + return writeRange(*this, value, 1); + } + template <> inline testing::Message& Message::operator<<(const std::vector& value) { diff --git a/apps/openmw_test_suite/esm3/testsaveload.cpp b/apps/openmw_test_suite/esm3/testsaveload.cpp index ff68d0d4f1..f8ef23e887 100644 --- a/apps/openmw_test_suite/esm3/testsaveload.cpp +++ b/apps/openmw_test_suite/esm3/testsaveload.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -410,6 +411,85 @@ namespace ESM EXPECT_EQ(result.mStringId, record.mStringId); } + TEST_P(Esm3SaveLoadRecordTest, aiSequenceAiWanderShouldNotChange) + { + AiSequence::AiWander record; + record.mData.mDistance = 1; + record.mData.mDuration = 2; + record.mData.mTimeOfDay = 3; + constexpr std::uint8_t idle[8] = { 4, 5, 6, 7, 8, 9, 10, 11 }; + static_assert(std::size(idle) == std::size(record.mData.mIdle)); + std::copy(std::begin(idle), std::end(idle), record.mData.mIdle); + record.mData.mShouldRepeat = 12; + record.mDurationData.mRemainingDuration = 13; + record.mStoredInitialActorPosition = true; + constexpr float initialActorPosition[3] = { 15, 16, 17 }; + static_assert(std::size(initialActorPosition) == std::size(record.mInitialActorPosition.mValues)); + std::copy( + std::begin(initialActorPosition), std::end(initialActorPosition), record.mInitialActorPosition.mValues); + + AiSequence::AiWander result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mDistance, record.mData.mDistance); + EXPECT_EQ(result.mData.mDuration, record.mData.mDuration); + EXPECT_EQ(result.mData.mTimeOfDay, record.mData.mTimeOfDay); + EXPECT_THAT(result.mData.mIdle, ElementsAreArray(record.mData.mIdle)); + EXPECT_EQ(result.mData.mShouldRepeat, record.mData.mShouldRepeat); + EXPECT_EQ(result.mDurationData.mRemainingDuration, record.mDurationData.mRemainingDuration); + EXPECT_EQ(result.mStoredInitialActorPosition, record.mStoredInitialActorPosition); + EXPECT_THAT(result.mInitialActorPosition.mValues, ElementsAreArray(record.mInitialActorPosition.mValues)); + } + + TEST_P(Esm3SaveLoadRecordTest, aiSequenceAiTravelShouldNotChange) + { + AiSequence::AiTravel record; + record.mData.mX = 1; + record.mData.mY = 2; + record.mData.mZ = 3; + record.mHidden = true; + record.mRepeat = true; + + AiSequence::AiTravel result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mX, record.mData.mX); + EXPECT_EQ(result.mData.mY, record.mData.mY); + EXPECT_EQ(result.mData.mZ, record.mData.mZ); + EXPECT_EQ(result.mHidden, record.mHidden); + EXPECT_EQ(result.mRepeat, record.mRepeat); + } + + TEST_P(Esm3SaveLoadRecordTest, aiSequenceAiEscortShouldNotChange) + { + AiSequence::AiEscort record; + record.mData.mX = 1; + record.mData.mY = 2; + record.mData.mZ = 3; + record.mData.mDuration = 4; + record.mTargetActorId = 5; + record.mTargetId = generateRandomRefId(32); + record.mCellId = generateRandomString(257); + record.mRemainingDuration = 6; + record.mRepeat = true; + + AiSequence::AiEscort result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mX, record.mData.mX); + EXPECT_EQ(result.mData.mY, record.mData.mY); + EXPECT_EQ(result.mData.mZ, record.mData.mZ); + if (GetParam() <= MaxOldAiPackageFormatVersion) + EXPECT_EQ(result.mData.mDuration, record.mRemainingDuration); + else + EXPECT_EQ(result.mData.mDuration, record.mData.mDuration); + EXPECT_EQ(result.mTargetActorId, record.mTargetActorId); + EXPECT_EQ(result.mTargetId, record.mTargetId); + EXPECT_EQ(result.mCellId, record.mCellId); + EXPECT_EQ(result.mRemainingDuration, record.mRemainingDuration); + EXPECT_EQ(result.mRepeat, record.mRepeat); + } + INSTANTIATE_TEST_SUITE_P(FormatVersions, Esm3SaveLoadRecordTest, ValuesIn(getFormats())); } } diff --git a/apps/openmw_test_suite/testing_util.hpp b/apps/openmw_test_suite/testing_util.hpp index 0c941053a7..b819848a8f 100644 --- a/apps/openmw_test_suite/testing_util.hpp +++ b/apps/openmw_test_suite/testing_util.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -60,7 +61,7 @@ namespace TestingOpenMW void listResources(VFS::FileMap& out) override { for (const auto& [key, value] : mFiles) - out.emplace(VFS::Path::normalizeFilename(key), value); + out.emplace(key, value); } bool contains(std::string_view file) const override { return mFiles.contains(file); } diff --git a/apps/wizard/main.cpp b/apps/wizard/main.cpp index e2b0d3874b..03ac24c8c0 100644 --- a/apps/wizard/main.cpp +++ b/apps/wizard/main.cpp @@ -28,8 +28,6 @@ int main(int argc, char* argv[]) app.setLibraryPaths(libraryPaths); #endif - QDir::setCurrent(dir.absolutePath()); - Wizard::MainWizard wizard; wizard.show(); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index 767d35d4f5..1d7d21b256 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -810,8 +810,7 @@ bool Wizard::UnshieldWorker::extractFile( if (!dir.mkpath(path)) return false; - QString fileName(path); - fileName.append(QString::fromUtf8(unshield_file_name(unshield, index))); + path.append(QString::fromUtf8(unshield_file_name(unshield, index))); // Calculate the percentage done int progress = (((float)counter / (float)unshield_file_count(unshield)) * 100); @@ -825,13 +824,13 @@ bool Wizard::UnshieldWorker::extractFile( emit textChanged(tr("Extracting: %1").arg(QString::fromUtf8(unshield_file_name(unshield, index)))); emit progressChanged(progress); - QByteArray array(fileName.toUtf8()); + QByteArray array(path.toUtf8()); success = unshield_file_save(unshield, index, array.constData()); if (!success) { qDebug() << "error"; - dir.remove(fileName); + dir.remove(path); } return success; diff --git a/components/bsa/ba2dx10file.cpp b/components/bsa/ba2dx10file.cpp index 945e8dd931..aa3f8d0581 100644 --- a/components/bsa/ba2dx10file.cpp +++ b/components/bsa/ba2dx10file.cpp @@ -76,7 +76,7 @@ namespace Bsa fail("Corrupted BSA"); } - mFolders[dirHash][{ nameHash, extHash }] = file; + mFolders[dirHash][{ nameHash, extHash }] = std::move(file); FileStruct fileStruct{}; mFiles.push_back(fileStruct); @@ -177,7 +177,7 @@ namespace Bsa return std::nullopt; // folder not found uint32_t fileHash = generateHash(fileName); - uint32_t extHash = *reinterpret_cast(ext.data() + 1); + uint32_t extHash = generateExtensionHash(ext); auto iter = it->second.find({ fileHash, extHash }); if (iter == it->second.end()) return std::nullopt; // file not found diff --git a/components/bsa/ba2file.cpp b/components/bsa/ba2file.cpp index b4fc7f9ec2..17cfb03866 100644 --- a/components/bsa/ba2file.cpp +++ b/components/bsa/ba2file.cpp @@ -46,4 +46,12 @@ namespace Bsa return result; } + uint32_t generateExtensionHash(std::string_view extension) + { + uint32_t result = 0; + for (size_t i = 0; i < 4 && i < extension.size() - 1; i++) + result |= static_cast(extension[i + 1]) << (8 * i); + return result; + } + } // namespace Bsa diff --git a/components/bsa/ba2file.hpp b/components/bsa/ba2file.hpp index e5a68d3caa..75a2ce8d61 100644 --- a/components/bsa/ba2file.hpp +++ b/components/bsa/ba2file.hpp @@ -7,6 +7,7 @@ namespace Bsa { uint32_t generateHash(const std::string& name); + uint32_t generateExtensionHash(std::string_view extension); } #endif diff --git a/components/bsa/ba2gnrlfile.cpp b/components/bsa/ba2gnrlfile.cpp index f3961a3bc4..02df12593c 100644 --- a/components/bsa/ba2gnrlfile.cpp +++ b/components/bsa/ba2gnrlfile.cpp @@ -172,7 +172,7 @@ namespace Bsa return FileRecord(); // folder not found, return default which has offset of sInvalidOffset uint32_t fileHash = generateHash(fileName); - uint32_t extHash = *reinterpret_cast(ext.data() + 1); + uint32_t extHash = generateExtensionHash(ext); auto iter = it->second.find({ fileHash, extHash }); if (iter == it->second.end()) return FileRecord(); // file not found, return default which has offset of sInvalidOffset diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 42c11628df..ad7c73d3d9 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -371,7 +371,7 @@ bool Config::GameSettings::writeFileWithComments(QFile& file) { if ((keyMatch.captured(1) + "=" + keyMatch.captured(2)) == keyVal) { - *iter = settingLine; + *iter = std::move(settingLine); break; } } diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 3a1b6fd77a..ec6313d6f1 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -180,8 +180,8 @@ namespace DetourNavigator if (!playerTileChanged && changedTiles.empty()) return; - const dtNavMeshParams params = *navMeshCacheItem->lockConst()->getImpl().getParams(); - const int maxTiles = std::min(mSettings.get().mMaxTilesNumber, params.maxTiles); + const int maxTiles + = std::min(mSettings.get().mMaxTilesNumber, navMeshCacheItem->lockConst()->getImpl().getParams()->maxTiles); std::unique_lock lock(mMutex); @@ -376,9 +376,10 @@ namespace DetourNavigator return JobStatus::Done; const auto playerTile = *mPlayerTile.lockConst(); - const auto params = *navMeshCacheItem->lockConst()->getImpl().getParams(); + const int maxTiles + = std::min(mSettings.get().mMaxTilesNumber, navMeshCacheItem->lockConst()->getImpl().getParams()->maxTiles); - if (!shouldAddTile(job.mChangedTile, playerTile, std::min(mSettings.get().mMaxTilesNumber, params.maxTiles))) + if (!shouldAddTile(job.mChangedTile, playerTile, maxTiles)) { Log(Debug::Debug) << "Ignore add tile by job " << job.mId << ": too far from player"; navMeshCacheItem->lock()->removeTile(job.mChangedTile); @@ -600,7 +601,7 @@ namespace DetourNavigator if (mSettings.get().mEnableRecastMeshFileNameRevision) recastMeshRevision = revision; if (mSettings.get().mEnableNavMeshFileNameRevision) - navMeshRevision = revision; + navMeshRevision = std::move(revision); } if (recastMesh && mSettings.get().mEnableWriteRecastMeshToFile) writeToFile(*recastMesh, diff --git a/components/detournavigator/gettilespositions.cpp b/components/detournavigator/gettilespositions.cpp index a3f46f3f85..343140633f 100644 --- a/components/detournavigator/gettilespositions.cpp +++ b/components/detournavigator/gettilespositions.cpp @@ -76,4 +76,13 @@ namespace DetourNavigator return {}; return TilesPositionsRange{ TilePosition(beginX, beginY), TilePosition(endX, endY) }; } + + TilesPositionsRange getUnion(const TilesPositionsRange& a, const TilesPositionsRange& b) noexcept + { + const int beginX = std::min(a.mBegin.x(), b.mBegin.x()); + const int endX = std::max(a.mEnd.x(), b.mEnd.x()); + const int beginY = std::min(a.mBegin.y(), b.mBegin.y()); + const int endY = std::max(a.mEnd.y(), b.mEnd.y()); + return TilesPositionsRange{ .mBegin = TilePosition(beginX, beginY), .mEnd = TilePosition(endX, endY) }; + } } diff --git a/components/detournavigator/gettilespositions.hpp b/components/detournavigator/gettilespositions.hpp index 32733224f3..66c3a90d1b 100644 --- a/components/detournavigator/gettilespositions.hpp +++ b/components/detournavigator/gettilespositions.hpp @@ -50,6 +50,8 @@ namespace DetourNavigator } TilesPositionsRange getIntersection(const TilesPositionsRange& a, const TilesPositionsRange& b) noexcept; + + TilesPositionsRange getUnion(const TilesPositionsRange& a, const TilesPositionsRange& b) noexcept; } #endif diff --git a/components/detournavigator/navmeshmanager.cpp b/components/detournavigator/navmeshmanager.cpp index 9fda0566d9..f4a82b850f 100644 --- a/components/detournavigator/navmeshmanager.cpp +++ b/components/detournavigator/navmeshmanager.cpp @@ -166,6 +166,7 @@ namespace DetourNavigator return; mLastRecastMeshManagerRevision = mRecastMeshManager.getRevision(); mPlayerTile = playerTile; + mRecastMeshManager.setRange(makeRange(playerTile, mSettings.mMaxTilesNumber), guard); const auto changedTiles = mRecastMeshManager.takeChangedTiles(guard); const TilesPositionsRange range = mRecastMeshManager.getLimitedObjectsRange(); for (const auto& [agentBounds, cached] : mCache) diff --git a/components/detournavigator/tilecachedrecastmeshmanager.cpp b/components/detournavigator/tilecachedrecastmeshmanager.cpp index 1e55719c13..0bab808300 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.cpp @@ -54,6 +54,15 @@ namespace DetourNavigator private: const std::optional> mImpl; }; + + TilesPositionsRange getIndexRange(const auto& index) + { + const auto bounds = index.bounds(); + return TilesPositionsRange{ + .mBegin = makeTilePosition(bounds.min_corner()), + .mEnd = makeTilePosition(bounds.max_corner()) + TilePosition(1, 1), + }; + } } TileCachedRecastMeshManager::TileCachedRecastMeshManager(const RecastSettings& settings) @@ -104,14 +113,28 @@ namespace DetourNavigator TilesPositionsRange TileCachedRecastMeshManager::getLimitedObjectsRange() const { - if (mObjects.empty()) - return {}; - const auto bounds = mObjectIndex.bounds(); - const TilesPositionsRange objectsRange{ - .mBegin = makeTilePosition(bounds.min_corner()), - .mEnd = makeTilePosition(bounds.max_corner()) + TilePosition(1, 1), - }; - return getIntersection(mRange, objectsRange); + std::optional result; + if (!mWater.empty()) + result = getIndexRange(mWaterIndex); + if (!mHeightfields.empty()) + { + const TilesPositionsRange range = getIndexRange(mHeightfieldIndex); + if (result.has_value()) + result = getUnion(*result, range); + else + result = range; + } + if (!mObjects.empty()) + { + const TilesPositionsRange range = getIndexRange(mObjectIndex); + if (result.has_value()) + result = getUnion(*result, range); + else + result = range; + } + if (result.has_value()) + return getIntersection(mRange, *result); + return {}; } void TileCachedRecastMeshManager::setWorldspace(std::string_view worldspace, const UpdateGuard* guard) diff --git a/components/esm/decompose.hpp b/components/esm/decompose.hpp new file mode 100644 index 0000000000..eb6f5070d4 --- /dev/null +++ b/components/esm/decompose.hpp @@ -0,0 +1,10 @@ +#ifndef OPENMW_COMPONENTS_ESM_DECOMPOSE_H +#define OPENMW_COMPONENTS_ESM_DECOMPOSE_H + +namespace ESM +{ + template + void decompose(T&& value, const auto& apply) = delete; +} + +#endif diff --git a/components/esm3/aisequence.cpp b/components/esm3/aisequence.cpp index 99c85db1bb..c316c2db86 100644 --- a/components/esm3/aisequence.cpp +++ b/components/esm3/aisequence.cpp @@ -3,32 +3,58 @@ #include "esmreader.hpp" #include "esmwriter.hpp" +#include + #include #include namespace ESM { + template T> + void decompose(T&& v, const auto& f) + { + f(v.mDistance, v.mDuration, v.mTimeOfDay, v.mIdle, v.mShouldRepeat); + } + + template T> + void decompose(T&& v, const auto& f) + { + std::uint32_t unused = 0; + f(v.mRemainingDuration, unused); + } + + template T> + void decompose(T&& v, const auto& f) + { + f(v.mX, v.mY, v.mZ); + } + + template T> + void decompose(T&& v, const auto& f) + { + f(v.mX, v.mY, v.mZ, v.mDuration); + } + namespace AiSequence { - void AiWander::load(ESMReader& esm) { - esm.getHNT("DATA", mData.mDistance, mData.mDuration, mData.mTimeOfDay, mData.mIdle, mData.mShouldRepeat); - esm.getHNT("STAR", mDurationData.mRemainingDuration, mDurationData.unused); // was mStartTime + esm.getNamedComposite("DATA", mData); + esm.getNamedComposite("STAR", mDurationData); // was mStartTime mStoredInitialActorPosition = esm.getHNOT("POS_", mInitialActorPosition.mValues); } void AiWander::save(ESMWriter& esm) const { - esm.writeHNT("DATA", mData); - esm.writeHNT("STAR", mDurationData); + esm.writeNamedComposite("DATA", mData); + esm.writeNamedComposite("STAR", mDurationData); // was mStartTime if (mStoredInitialActorPosition) - esm.writeHNT("POS_", mInitialActorPosition); + esm.writeHNT("POS_", mInitialActorPosition.mValues); } void AiTravel::load(ESMReader& esm) { - esm.getHNT("DATA", mData.mX, mData.mY, mData.mZ); + esm.getNamedComposite("DATA", mData); esm.getHNT(mHidden, "HIDD"); mRepeat = false; esm.getHNOT(mRepeat, "REPT"); @@ -36,7 +62,7 @@ namespace ESM void AiTravel::save(ESMWriter& esm) const { - esm.writeHNT("DATA", mData); + esm.writeNamedComposite("DATA", mData); esm.writeHNT("HIDD", mHidden); if (mRepeat) esm.writeHNT("REPT", mRepeat); @@ -44,7 +70,7 @@ namespace ESM void AiEscort::load(ESMReader& esm) { - esm.getHNT("DATA", mData.mX, mData.mY, mData.mZ, mData.mDuration); + esm.getNamedComposite("DATA", mData); mTargetId = esm.getHNRefId("TARG"); mTargetActorId = -1; esm.getHNOT(mTargetActorId, "TAID"); @@ -64,7 +90,7 @@ namespace ESM void AiEscort::save(ESMWriter& esm) const { - esm.writeHNT("DATA", mData); + esm.writeNamedComposite("DATA", mData); esm.writeHNRefId("TARG", mTargetId); esm.writeHNT("TAID", mTargetActorId); esm.writeHNT("DURA", mRemainingDuration); @@ -76,7 +102,7 @@ namespace ESM void AiFollow::load(ESMReader& esm) { - esm.getHNT("DATA", mData.mX, mData.mY, mData.mZ, mData.mDuration); + esm.getNamedComposite("DATA", mData); mTargetId = esm.getHNRefId("TARG"); mTargetActorId = -1; esm.getHNOT(mTargetActorId, "TAID"); @@ -101,7 +127,7 @@ namespace ESM void AiFollow::save(ESMWriter& esm) const { - esm.writeHNT("DATA", mData); + esm.writeNamedComposite("DATA", mData); esm.writeHNRefId("TARG", mTargetId); esm.writeHNT("TAID", mTargetActorId); esm.writeHNT("DURA", mRemainingDuration); diff --git a/components/esm3/aisequence.hpp b/components/esm3/aisequence.hpp index 107fdf3bdb..d6d6259f6b 100644 --- a/components/esm3/aisequence.hpp +++ b/components/esm3/aisequence.hpp @@ -36,32 +36,31 @@ namespace ESM virtual ~AiPackage() {} }; -#pragma pack(push, 1) struct AiWanderData { int16_t mDistance; int16_t mDuration; - unsigned char mTimeOfDay; - unsigned char mIdle[8]; - unsigned char mShouldRepeat; + std::uint8_t mTimeOfDay; + std::uint8_t mIdle[8]; + std::uint8_t mShouldRepeat; }; + struct AiWanderDuration { float mRemainingDuration; - int32_t unused; }; + struct AiTravelData { float mX, mY, mZ; }; + struct AiEscortData { float mX, mY, mZ; int16_t mDuration; }; -#pragma pack(pop) - struct AiWander : AiPackage { AiWanderData mData; diff --git a/components/esm3/esmreader.hpp b/components/esm3/esmreader.hpp index 461f154001..276adf749c 100644 --- a/components/esm3/esmreader.hpp +++ b/components/esm3/esmreader.hpp @@ -12,8 +12,10 @@ #include +#include "components/esm/decompose.hpp" #include "components/esm/esmcommon.hpp" #include "components/esm/refid.hpp" + #include "loadtes3.hpp" namespace ESM @@ -177,6 +179,16 @@ namespace ESM (getT(args), ...); } + void getNamedComposite(NAME name, auto& value) + { + decompose(value, [&](auto&... args) { getHNT(name, args...); }); + } + + void getComposite(auto& value) + { + decompose(value, [&](auto&... args) { (getT(args), ...); }); + } + template >> void skipHT() { diff --git a/components/esm3/esmwriter.hpp b/components/esm3/esmwriter.hpp index 5086005b1f..101246fe43 100644 --- a/components/esm3/esmwriter.hpp +++ b/components/esm3/esmwriter.hpp @@ -5,6 +5,7 @@ #include #include +#include "components/esm/decompose.hpp" #include "components/esm/esmcommon.hpp" #include "components/esm/refid.hpp" @@ -121,6 +122,20 @@ namespace ESM endRecord(name); } + void writeNamedComposite(NAME name, const auto& value) + { + decompose(value, [&](const auto&... args) { + startSubRecord(name); + (writeT(args), ...); + endRecord(name); + }); + } + + void writeComposite(const auto& value) + { + decompose(value, [&](const auto&... args) { (writeT(args), ...); }); + } + // Prevent using writeHNT with strings. This already happened by accident and results in // state being discarded without any error on writing or reading it. :( // writeHNString and friends must be used instead. @@ -132,7 +147,7 @@ namespace ESM void writeHNT(NAME name, const T (&data)[size], int) = delete; template - void writeHNT(NAME name, const T& data, int size) + void writeHNT(NAME name, const T& data, std::size_t size) { startSubRecord(name); writeT(data, size); diff --git a/components/esm3/inventorystate.cpp b/components/esm3/inventorystate.cpp index 1947be23e9..f3dce52f29 100644 --- a/components/esm3/inventorystate.cpp +++ b/components/esm3/inventorystate.cpp @@ -74,7 +74,7 @@ namespace ESM esm.getHNT(multiplier, "MULT"); params.emplace_back(rand, multiplier); } - mPermanentMagicEffectMagnitudes[id] = params; + mPermanentMagicEffectMagnitudes[id] = std::move(params); } while (esm.isNextSub("EQUI")) diff --git a/components/esm3/loadmgef.cpp b/components/esm3/loadmgef.cpp index 686afbc34a..8d5b99b0c3 100644 --- a/components/esm3/loadmgef.cpp +++ b/components/esm3/loadmgef.cpp @@ -631,7 +631,7 @@ namespace ESM { auto name = sIndexNameToIndexMap.find(effect); if (name == sIndexNameToIndexMap.end()) - throw std::runtime_error("Unimplemented effect " + std::string(effect)); + return -1; return name->second; } diff --git a/components/esm3/loadscpt.cpp b/components/esm3/loadscpt.cpp index c1dc759cdc..f79f4989ef 100644 --- a/components/esm3/loadscpt.cpp +++ b/components/esm3/loadscpt.cpp @@ -4,12 +4,19 @@ #include #include +#include #include "esmreader.hpp" #include "esmwriter.hpp" namespace ESM { + template T> + void decompose(T&& v, const auto& f) + { + f(v.mNumShorts, v.mNumLongs, v.mNumFloats, v.mScriptDataSize, v.mStringTableSize); + } + void Script::loadSCVR(ESMReader& esm) { uint32_t s = mData.mStringTableSize; @@ -99,11 +106,7 @@ namespace ESM { esm.getSubHeader(); mId = esm.getMaybeFixedRefIdSize(32); - esm.getT(mData.mNumShorts); - esm.getT(mData.mNumLongs); - esm.getT(mData.mNumFloats); - esm.getT(mData.mScriptDataSize); - esm.getT(mData.mStringTableSize); + esm.getComposite(mData); hasHeader = true; break; @@ -157,7 +160,7 @@ namespace ESM esm.startSubRecord("SCHD"); esm.writeMaybeFixedSizeRefId(mId, 32); - esm.writeT(mData, 20); + esm.writeComposite(mData); esm.endRecord("SCHD"); if (isDeleted) diff --git a/components/esm3/savedgame.cpp b/components/esm3/savedgame.cpp index 3ffe062d76..0dc1fb0653 100644 --- a/components/esm3/savedgame.cpp +++ b/components/esm3/savedgame.cpp @@ -3,6 +3,8 @@ #include "esmreader.hpp" #include "esmwriter.hpp" +#include "../misc/algorithm.hpp" + namespace ESM { void SavedGame::load(ESMReader& esm) @@ -67,7 +69,9 @@ namespace ESM std::vector missingFiles; for (const std::string& contentFile : mContentFiles) { - if (std::find(allContentFiles.begin(), allContentFiles.end(), contentFile) == allContentFiles.end()) + auto it = std::find_if(allContentFiles.begin(), allContentFiles.end(), + [&](const std::string& file) { return Misc::StringUtils::ciEqual(file, contentFile); }); + if (it == allContentFiles.end()) { missingFiles.emplace_back(contentFile); } diff --git a/components/esm3/spellstate.cpp b/components/esm3/spellstate.cpp index 41591f56b7..39c98e7c0f 100644 --- a/components/esm3/spellstate.cpp +++ b/components/esm3/spellstate.cpp @@ -33,7 +33,7 @@ namespace ESM state.mPurgedEffects.insert(index); } - mSpellParams[id] = state; + mSpellParams[id] = std::move(state); mSpells.emplace_back(id); } } @@ -69,7 +69,7 @@ namespace ESM esm.getHNT(info.mMagnitude, "MAGN"); permEffectList.push_back(info); } - mPermanentSpellEffects[spellId] = permEffectList; + mPermanentSpellEffects[spellId] = std::move(permEffectList); } // Obsolete diff --git a/components/esm4/loadfurn.cpp b/components/esm4/loadfurn.cpp index 40ea04955e..41ddca07a2 100644 --- a/components/esm4/loadfurn.cpp +++ b/components/esm4/loadfurn.cpp @@ -50,7 +50,7 @@ void ESM4::Furniture::load(ESM4::Reader& reader) reader.getLocalizedString(name); // FIXME: subsequent FULL subrecords name object combinations (FO4) if (mFullName.empty()) - mFullName = name; + mFullName = std::move(name); break; } case ESM4::SUB_MODL: diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index d8cf964f71..a00cca0904 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -586,7 +586,7 @@ namespace ESMTerrain Misc::StringUtils::replaceLast(texture_, ".", mNormalHeightMapPattern + "."); if (mVFS->exists(texture_)) { - info.mNormalMap = texture_; + info.mNormalMap = std::move(texture_); info.mParallax = true; } else @@ -594,7 +594,7 @@ namespace ESMTerrain texture_ = texture; Misc::StringUtils::replaceLast(texture_, ".", mNormalMapPattern + "."); if (mVFS->exists(texture_)) - info.mNormalMap = texture_; + info.mNormalMap = std::move(texture_); } } @@ -604,7 +604,7 @@ namespace ESMTerrain Misc::StringUtils::replaceLast(texture_, ".", mSpecularMapPattern + "."); if (mVFS->exists(texture_)) { - info.mDiffuseMap = texture_; + info.mDiffuseMap = std::move(texture_); info.mSpecular = true; } } diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index ece30e5b3f..943f514676 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -309,7 +309,7 @@ namespace Files tempPath /= str.substr(pos + 1, str.length() - pos); } - path = tempPath; + path = std::move(tempPath); } else { diff --git a/components/files/macospath.cpp b/components/files/macospath.cpp index 2d0a409782..4b37c2fb26 100644 --- a/components/files/macospath.cpp +++ b/components/files/macospath.cpp @@ -5,13 +5,41 @@ #include #include #include +#include #include #include +#include +#include #include namespace { + std::filesystem::path getBinaryPath() + { + uint32_t bufsize = 0; + _NSGetExecutablePath(nullptr, &bufsize); + + std::vector buf(bufsize); + + if (_NSGetExecutablePath(buf.data(), &bufsize) == 0) + { + std::filesystem::path path = std::filesystem::path(buf.begin(), buf.end()); + + if (std::filesystem::is_symlink(path)) + { + return std::filesystem::read_symlink(path); + } + + return path; + } + else + { + Log(Debug::Warning) << "Not enough buffer size to get executable path: " << bufsize; + throw std::runtime_error("Failed to get executable path"); + } + } + std::filesystem::path getUserHome() { const char* dir = getenv("HOME"); @@ -36,6 +64,11 @@ namespace Files MacOsPath::MacOsPath(const std::string& application_name) : mName(application_name) { + std::filesystem::path binary_path = getBinaryPath(); + std::error_code ec; + std::filesystem::current_path(binary_path.parent_path(), ec); + if (ec.value() != 0) + Log(Debug::Warning) << "Error " << ec.message() << " when changing current directory"; } std::filesystem::path MacOsPath::getUserConfigPath() const diff --git a/components/fx/technique.hpp b/components/fx/technique.hpp index 0d17128e56..fa66996aeb 100644 --- a/components/fx/technique.hpp +++ b/components/fx/technique.hpp @@ -55,7 +55,7 @@ namespace fx osg::ref_ptr mRenderTexture; bool mResolve = false; Types::SizeProxy mSize; - bool mMipMap; + bool mMipMap = false; SubPass(const SubPass& other, const osg::CopyOp& copyOp = osg::CopyOp::SHALLOW_COPY) : mStateSet(new osg::StateSet(*other.mStateSet, copyOp)) diff --git a/components/fx/types.hpp b/components/fx/types.hpp index 0f33d29e1a..829bf176b7 100644 --- a/components/fx/types.hpp +++ b/components/fx/types.hpp @@ -29,16 +29,6 @@ namespace fx std::optional mWidth; std::optional mHeight; - SizeProxy() = default; - - SizeProxy(const SizeProxy& other) - : mWidthRatio(other.mWidthRatio) - , mHeightRatio(other.mHeightRatio) - , mWidth(other.mWidth) - , mHeight(other.mHeight) - { - } - std::tuple get(int width, int height) const { int scaledWidth = width; @@ -64,16 +54,6 @@ namespace fx SizeProxy mSize; bool mMipMap = false; osg::Vec4f mClearColor = osg::Vec4f(0.0, 0.0, 0.0, 1.0); - - RenderTarget() = default; - - RenderTarget(const RenderTarget& other) - : mTarget(other.mTarget) - , mSize(other.mSize) - , mMipMap(other.mMipMap) - , mClearColor(other.mClearColor) - { - } }; template diff --git a/components/lua/asyncpackage.cpp b/components/lua/asyncpackage.cpp index b60238de13..8316ab2cde 100644 --- a/components/lua/asyncpackage.cpp +++ b/components/lua/asyncpackage.cpp @@ -85,7 +85,7 @@ namespace LuaUtil auto initializer = [](sol::table hiddenData) { ScriptId id = hiddenData[ScriptsContainer::sScriptIdKey]; - return AsyncPackageId{ id.mContainer, id.mIndex, hiddenData }; + return AsyncPackageId{ id.mContainer, id.mIndex, std::move(hiddenData) }; }; return sol::make_object(lua, initializer); } diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp index da39cca1b8..5a54cd91b5 100644 --- a/components/lua_ui/element.cpp +++ b/components/lua_ui/element.cpp @@ -38,7 +38,7 @@ namespace LuaUi if (typeField != sol::nil && templateType != type) throw std::logic_error(std::string("Template layout type ") + type + std::string(" doesn't match template type ") + templateType); - type = templateType; + type = std::move(templateType); } return type; } diff --git a/components/lua_ui/scriptsettings.cpp b/components/lua_ui/scriptsettings.cpp index 514e6ce632..69a5ee88e5 100644 --- a/components/lua_ui/scriptsettings.cpp +++ b/components/lua_ui/scriptsettings.cpp @@ -21,7 +21,7 @@ namespace LuaUi Log(Debug::Warning) << "A script settings page has an empty name"; if (!element.get()) Log(Debug::Warning) << "A script settings page has no UI element assigned"; - return { std::move(name), std::move(searchHints), element }; + return { std::move(name), std::move(searchHints), std::move(element) }; } } diff --git a/components/misc/concepts.hpp b/components/misc/concepts.hpp new file mode 100644 index 0000000000..d8573e94ab --- /dev/null +++ b/components/misc/concepts.hpp @@ -0,0 +1,13 @@ +#ifndef OPENMW_COMPONENTS_MISC_CONCEPTS_H +#define OPENMW_COMPONENTS_MISC_CONCEPTS_H + +#include +#include + +namespace Misc +{ + template + concept SameAsWithoutCvref = std::same_as, std::remove_cvref_t>; +} + +#endif diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index d6d063a254..8a0b0bf930 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -24,7 +24,7 @@ namespace Nif { - Reader::Reader(NIFFile& file) + Reader::Reader(NIFFile& file, const ToUTF8::StatelessUtf8Encoder* encoder) : mVersion(file.mVersion) , mUserVersion(file.mUserVersion) , mBethVersion(file.mBethVersion) @@ -33,6 +33,7 @@ namespace Nif , mRecords(file.mRecords) , mRoots(file.mRoots) , mUseSkinning(file.mUseSkinning) + , mEncoder(encoder) { } @@ -519,7 +520,7 @@ namespace Nif const std::array fileHash = Files::getHash(mFilename, *stream); mHash.append(reinterpret_cast(fileHash.data()), fileHash.size() * sizeof(std::uint64_t)); - NIFStream nif(*this, std::move(stream)); + NIFStream nif(*this, std::move(stream), mEncoder); // Check the header string std::string head = nif.getVersionString(); @@ -672,7 +673,7 @@ namespace Nif assert(r != nullptr); assert(r->recType != RC_MISSING); - r->recName = rec; + r->recName = std::move(rec); r->recIndex = i; r->read(&nif); mRecords[i] = std::move(r); diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp index 6f0030af47..993e9b7eea 100644 --- a/components/nif/niffile.hpp +++ b/components/nif/niffile.hpp @@ -11,6 +11,11 @@ #include "record.hpp" +namespace ToUTF8 +{ + class StatelessUtf8Encoder; +} + namespace Nif { @@ -112,6 +117,7 @@ namespace Nif std::vector mStrings; bool& mUseSkinning; + const ToUTF8::StatelessUtf8Encoder* mEncoder; static std::atomic_bool sLoadUnsupportedFiles; static std::atomic_bool sWriteNifDebugLog; @@ -122,7 +128,7 @@ namespace Nif public: /// Open a NIF stream. The name is used for error messages. - explicit Reader(NIFFile& file); + explicit Reader(NIFFile& file, const ToUTF8::StatelessUtf8Encoder* encoder); /// Parse the file void parse(Files::IStreamPtr&& stream); diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index 2eba746ccf..f960e8d972 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -4,6 +4,8 @@ #include "niffile.hpp" +#include "../to_utf8/to_utf8.hpp" + namespace { @@ -58,6 +60,8 @@ namespace Nif size_t end = str.find('\0'); if (end != std::string::npos) str.erase(end); + if (mEncoder) + str = mEncoder->getUtf8(str, ToUTF8::BufferAllocationPolicy::UseGrowFactor, mBuffer); return str; } diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp index 95205c4fda..062f7c6512 100644 --- a/components/nif/nifstream.hpp +++ b/components/nif/nifstream.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,11 @@ #include "niftypes.hpp" +namespace ToUTF8 +{ + class StatelessUtf8Encoder; +} + namespace Nif { @@ -67,11 +73,15 @@ namespace Nif { const Reader& mReader; Files::IStreamPtr mStream; + const ToUTF8::StatelessUtf8Encoder* mEncoder; + std::string mBuffer; public: - explicit NIFStream(const Reader& reader, Files::IStreamPtr&& stream) + explicit NIFStream( + const Reader& reader, Files::IStreamPtr&& stream, const ToUTF8::StatelessUtf8Encoder* encoder) : mReader(reader) , mStream(std::move(stream)) + , mEncoder(encoder) { } diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 2f7574d68b..8d46b0f751 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -1660,7 +1660,7 @@ namespace NifOsg && bsTriShape->mVertDesc.mFlags & Nif::BSVertexDesc::VertexAttribute::Skinned) { osg::ref_ptr rig(new SceneUtil::RigGeometry); - rig->setSourceGeometry(geometry); + rig->setSourceGeometry(std::move(geometry)); const Nif::BSSkinInstance* skin = static_cast(bsTriShape->mSkin.getPtr()); const Nif::BSSkinBoneData* data = skin->mData.getPtr(); diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 360b92ffc0..70348e956d 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -39,6 +39,13 @@ namespace Resource const_cast(trishape->getChildShape()), trishape->getLocalScaling())); } + if (shape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE) + { + const btBvhTriangleMeshShape* trishape = static_cast(shape); + return CollisionShapePtr(new btScaledBvhTriangleMeshShape( + const_cast(trishape), btVector3(1.f, 1.f, 1.f))); + } + if (shape->getShapeType() == BOX_SHAPE_PROXYTYPE) { const btBoxShape* boxshape = static_cast(shape); diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index 574d761d09..68b7adbe9a 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -123,7 +123,7 @@ namespace Resource mergedAnimationTrack->addChannel(channel.get()->clone()); } - callback->addMergedAnimationTrack(mergedAnimationTrack); + callback->addMergedAnimationTrack(std::move(mergedAnimationTrack)); float startTime = animation->getStartTime(); float stopTime = startTime + animation->getDuration(); @@ -207,9 +207,11 @@ namespace Resource namespace Resource { - KeyframeManager::KeyframeManager(const VFS::Manager* vfs, SceneManager* sceneManager, double expiryDelay) + KeyframeManager::KeyframeManager(const VFS::Manager* vfs, SceneManager* sceneManager, double expiryDelay, + const ToUTF8::StatelessUtf8Encoder* encoder) : ResourceManager(vfs, expiryDelay) , mSceneManager(sceneManager) + , mEncoder(encoder) { } @@ -226,7 +228,7 @@ namespace Resource if (Misc::getFileExtension(normalized) == "kf") { auto file = std::make_shared(normalized); - Nif::Reader reader(*file); + Nif::Reader reader(*file, mEncoder); reader.parse(mVFS->getNormalized(normalized)); NifOsg::Loader::loadKf(*file, *loaded.get()); } @@ -237,7 +239,7 @@ namespace Resource = dynamic_cast(scene->getUpdateCallback()); if (bam) { - Resource::RetrieveAnimationsVisitor rav(*loaded.get(), bam, normalized, mVFS); + Resource::RetrieveAnimationsVisitor rav(*loaded.get(), std::move(bam), normalized, mVFS); scene->accept(rav); } } diff --git a/components/resource/keyframemanager.hpp b/components/resource/keyframemanager.hpp index 0c92553949..ed8d4a04ab 100644 --- a/components/resource/keyframemanager.hpp +++ b/components/resource/keyframemanager.hpp @@ -9,6 +9,11 @@ #include "resourcemanager.hpp" +namespace ToUTF8 +{ + class StatelessUtf8Encoder; +} + namespace Resource { /// @brief extract animations from OSG formats to OpenMW's animation system @@ -48,7 +53,8 @@ namespace Resource class KeyframeManager : public ResourceManager { public: - explicit KeyframeManager(const VFS::Manager* vfs, SceneManager* sceneManager, double expiryDelay); + explicit KeyframeManager(const VFS::Manager* vfs, SceneManager* sceneManager, double expiryDelay, + const ToUTF8::StatelessUtf8Encoder* encoder); ~KeyframeManager() = default; /// Retrieve a read-only keyframe resource by name (case-insensitive). @@ -59,6 +65,7 @@ namespace Resource private: SceneManager* mSceneManager; + const ToUTF8::StatelessUtf8Encoder* mEncoder; }; } diff --git a/components/resource/niffilemanager.cpp b/components/resource/niffilemanager.cpp index 5e457cdfaa..352d367f9b 100644 --- a/components/resource/niffilemanager.cpp +++ b/components/resource/niffilemanager.cpp @@ -24,21 +24,22 @@ namespace Resource { } - NifFileHolder() {} + NifFileHolder() = default; META_Object(Resource, NifFileHolder) Nif::NIFFilePtr mNifFile; }; - NifFileManager::NifFileManager(const VFS::Manager* vfs) + NifFileManager::NifFileManager(const VFS::Manager* vfs, const ToUTF8::StatelessUtf8Encoder* encoder) // NIF files aren't needed any more once the converted objects are cached in SceneManager / BulletShapeManager, // so no point in using an expiry delay. : ResourceManager(vfs, 0) + , mEncoder(encoder) { } - NifFileManager::~NifFileManager() {} + NifFileManager::~NifFileManager() = default; Nif::NIFFilePtr NifFileManager::get(const std::string& name) { @@ -48,7 +49,7 @@ namespace Resource else { auto file = std::make_shared(name); - Nif::Reader reader(*file); + Nif::Reader reader(*file, mEncoder); reader.parse(mVFS->get(name)); obj = new NifFileHolder(file); mCache->addEntryToObjectCache(name, obj); diff --git a/components/resource/niffilemanager.hpp b/components/resource/niffilemanager.hpp index 5aef3f3016..dab4b70748 100644 --- a/components/resource/niffilemanager.hpp +++ b/components/resource/niffilemanager.hpp @@ -5,6 +5,11 @@ #include "resourcemanager.hpp" +namespace ToUTF8 +{ + class StatelessUtf8Encoder; +} + namespace Resource { @@ -12,8 +17,10 @@ namespace Resource /// @note May be used from any thread. class NifFileManager : public ResourceManager { + const ToUTF8::StatelessUtf8Encoder* mEncoder; + public: - NifFileManager(const VFS::Manager* vfs); + NifFileManager(const VFS::Manager* vfs, const ToUTF8::StatelessUtf8Encoder* encoder); ~NifFileManager(); /// Retrieve a NIF file from the cache, or load it from the VFS if not cached yet. diff --git a/components/resource/resourcesystem.cpp b/components/resource/resourcesystem.cpp index 0bee08e9ac..65a83a60ab 100644 --- a/components/resource/resourcesystem.cpp +++ b/components/resource/resourcesystem.cpp @@ -10,13 +10,14 @@ namespace Resource { - ResourceSystem::ResourceSystem(const VFS::Manager* vfs, double expiryDelay) + ResourceSystem::ResourceSystem( + const VFS::Manager* vfs, double expiryDelay, const ToUTF8::StatelessUtf8Encoder* encoder) : mVFS(vfs) { - mNifFileManager = std::make_unique(vfs); + mNifFileManager = std::make_unique(vfs, encoder); mImageManager = std::make_unique(vfs, expiryDelay); mSceneManager = std::make_unique(vfs, mImageManager.get(), mNifFileManager.get(), expiryDelay); - mKeyframeManager = std::make_unique(vfs, mSceneManager.get(), expiryDelay); + mKeyframeManager = std::make_unique(vfs, mSceneManager.get(), expiryDelay, encoder); addResourceManager(mNifFileManager.get()); addResourceManager(mKeyframeManager.get()); diff --git a/components/resource/resourcesystem.hpp b/components/resource/resourcesystem.hpp index d06ac79640..f7f09b9277 100644 --- a/components/resource/resourcesystem.hpp +++ b/components/resource/resourcesystem.hpp @@ -15,6 +15,11 @@ namespace osg class State; } +namespace ToUTF8 +{ + class StatelessUtf8Encoder; +} + namespace Resource { @@ -30,7 +35,8 @@ namespace Resource class ResourceSystem { public: - explicit ResourceSystem(const VFS::Manager* vfs, double expiryDelay); + explicit ResourceSystem( + const VFS::Manager* vfs, double expiryDelay, const ToUTF8::StatelessUtf8Encoder* encoder); ~ResourceSystem(); SceneManager* getSceneManager(); diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 25abcfd0d8..787f2e8441 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -867,7 +867,7 @@ namespace Resource return static_cast(mErrorMarker->clone(osg::CopyOp::DEEP_COPY_ALL)); } - osg::ref_ptr SceneManager::getTemplate(const std::string& name, bool compile) + osg::ref_ptr SceneManager::getTemplate(std::string_view name, bool compile) { std::string normalized = VFS::Path::normalizeFilename(name); @@ -927,7 +927,7 @@ namespace Resource } } - osg::ref_ptr SceneManager::getInstance(const std::string& name) + osg::ref_ptr SceneManager::getInstance(std::string_view name) { osg::ref_ptr scene = getTemplate(name); return getInstance(scene); @@ -968,7 +968,7 @@ namespace Resource return cloned; } - osg::ref_ptr SceneManager::getInstance(const std::string& name, osg::Group* parentNode) + osg::ref_ptr SceneManager::getInstance(std::string_view name, osg::Group* parentNode) { osg::ref_ptr cloned = getInstance(name); attachTo(cloned, parentNode); diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index c7663a4d91..12900441de 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -157,7 +157,7 @@ namespace Resource /// @note If the given filename does not exist or fails to load, an error marker mesh will be used instead. /// If even the error marker mesh can not be found, an exception is thrown. /// @note Thread safe. - osg::ref_ptr getTemplate(const std::string& name, bool compile = true); + osg::ref_ptr getTemplate(std::string_view name, bool compile = true); /// Clone osg::Node safely. /// @note Thread safe. @@ -172,12 +172,12 @@ namespace Resource /// Instance the given scene template. /// @see getTemplate /// @note Thread safe. - osg::ref_ptr getInstance(const std::string& name); + osg::ref_ptr getInstance(std::string_view name); /// Instance the given scene template and immediately attach it to a parent node /// @see getTemplate /// @note Not thread safe, unless parentNode is not part of the main scene graph yet. - osg::ref_ptr getInstance(const std::string& name, osg::Group* parentNode); + osg::ref_ptr getInstance(std::string_view name, osg::Group* parentNode); /// Attach the given scene instance to the given parent node /// @note You should have the parentNode in its intended position before calling this method, diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index 6ff366f76e..ab600de11d 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -1,6 +1,7 @@ #include "util.hpp" #include +#include #include #include @@ -17,25 +18,45 @@ namespace SceneUtil { - - class FindLowestUnusedTexUnitVisitor : public osg::NodeVisitor + namespace { - public: - FindLowestUnusedTexUnitVisitor() - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mLowestUnusedTexUnit(0) + std::array generateGlowTextureNames() { + std::array result; + for (std::size_t i = 0; i < result.size(); ++i) + { + std::stringstream stream; + stream << "textures/magicitem/caust"; + stream << std::setw(2); + stream << std::setfill('0'); + stream << i; + stream << ".dds"; + result[i] = std::move(stream).str(); + } + return result; } - void apply(osg::Node& node) override - { - if (osg::StateSet* stateset = node.getStateSet()) - mLowestUnusedTexUnit = std::max(mLowestUnusedTexUnit, int(stateset->getTextureAttributeList().size())); + const std::array glowTextureNames = generateGlowTextureNames(); - traverse(node); - } - int mLowestUnusedTexUnit; - }; + struct FindLowestUnusedTexUnitVisitor : public osg::NodeVisitor + { + FindLowestUnusedTexUnitVisitor() + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + { + } + + void apply(osg::Node& node) override + { + if (osg::StateSet* stateset = node.getStateSet()) + mLowestUnusedTexUnit + = std::max(mLowestUnusedTexUnit, int(stateset->getTextureAttributeList().size())); + + traverse(node); + } + + int mLowestUnusedTexUnit = 0; + }; + } GlowUpdater::GlowUpdater(int texUnit, const osg::Vec4f& color, const std::vector>& textures, osg::Node* node, float duration, @@ -197,16 +218,9 @@ namespace SceneUtil const osg::Vec4f& glowColor, float glowDuration) { std::vector> textures; - for (int i = 0; i < 32; ++i) + for (const std::string& name : glowTextureNames) { - std::stringstream stream; - stream << "textures/magicitem/caust"; - stream << std::setw(2); - stream << std::setfill('0'); - stream << i; - stream << ".dds"; - - osg::ref_ptr image = resourceSystem->getImageManager()->getImage(stream.str()); + osg::ref_ptr image = resourceSystem->getImageManager()->getImage(name); osg::ref_ptr tex(new osg::Texture2D(image)); tex->setName("envMap"); tex->setWrap(osg::Texture::WRAP_S, osg::Texture2D::REPEAT); @@ -234,7 +248,7 @@ namespace SceneUtil } writableStateSet->setTextureAttributeAndModes(texUnit, textures.front(), osg::StateAttribute::ON); writableStateSet->addUniform(new osg::Uniform("envMapColor", glowColor)); - resourceSystem->getSceneManager()->recreateShaders(node); + resourceSystem->getSceneManager()->recreateShaders(std::move(node)); return glowUpdater; } diff --git a/components/settings/parser.cpp b/components/settings/parser.cpp index 5ec41c5f4b..ff6bc5ca48 100644 --- a/components/settings/parser.cpp +++ b/components/settings/parser.cpp @@ -77,7 +77,7 @@ void Settings::SettingsFileParser::loadSettingsFile( Misc::StringUtils::trim(value); if (overrideExisting) - settings[std::make_pair(currentCategory, setting)] = value; + settings[std::make_pair(currentCategory, setting)] = std::move(value); else if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) fail(std::string("duplicate setting: [" + currentCategory + "] " + setting)); } diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 70464f571e..e281f64448 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -740,7 +740,7 @@ namespace Shader auto program = mShaderManager.getProgram(shaderPrefix, defineMap, mProgramTemplate); writableStateSet->setAttributeAndModes(program, osg::StateAttribute::ON); - addedState->setAttributeAndModes(program); + addedState->setAttributeAndModes(std::move(program)); for (const auto& [unit, name] : reqs.mTextures) { @@ -934,13 +934,13 @@ namespace Shader { osg::ref_ptr sourceGeometry = rig->getSourceGeometry(); if (sourceGeometry && adjustGeometry(*sourceGeometry, reqs)) - rig->setSourceGeometry(sourceGeometry); + rig->setSourceGeometry(std::move(sourceGeometry)); } else if (auto morph = dynamic_cast(&drawable)) { osg::ref_ptr sourceGeometry = morph->getSourceGeometry(); if (sourceGeometry && adjustGeometry(*sourceGeometry, reqs)) - morph->setSourceGeometry(sourceGeometry); + morph->setSourceGeometry(std::move(sourceGeometry)); } else if (auto osgaRig = dynamic_cast(&drawable)) { @@ -948,8 +948,8 @@ namespace Shader osg::ref_ptr sourceGeometry = sourceOsgaRigGeometry->getSourceGeometry(); if (sourceGeometry && adjustGeometry(*sourceGeometry, reqs)) { - sourceOsgaRigGeometry->setSourceGeometry(sourceGeometry); - osgaRig->setSourceRigGeometry(sourceOsgaRigGeometry); + sourceOsgaRigGeometry->setSourceGeometry(std::move(sourceGeometry)); + osgaRig->setSourceRigGeometry(std::move(sourceOsgaRigGeometry)); } } diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 93a9c563af..9c409b3bc2 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -145,7 +145,7 @@ namespace Terrain osg::Callback* World::getHeightCullCallback(float highz, unsigned int mask) { - if (!mHeightCullCallback) + if (!mHeightCullCallback || mTerrainRoot->getNumChildren() == 0) return nullptr; mHeightCullCallback->setHighZ(highz); diff --git a/components/to_utf8/to_utf8.hpp b/components/to_utf8/to_utf8.hpp index 11a466e44c..80af6586c9 100644 --- a/components/to_utf8/to_utf8.hpp +++ b/components/to_utf8/to_utf8.hpp @@ -68,7 +68,7 @@ namespace ToUTF8 /// ASCII-only string. Otherwise returns a view to the input. std::string_view getLegacyEnc(std::string_view input); - StatelessUtf8Encoder getStatelessEncoder() const { return mImpl; } + const StatelessUtf8Encoder& getStatelessEncoder() const { return mImpl; } private: std::string mBuffer; diff --git a/components/vfs/archive.hpp b/components/vfs/archive.hpp index 79c876b391..42b88219d7 100644 --- a/components/vfs/archive.hpp +++ b/components/vfs/archive.hpp @@ -1,27 +1,13 @@ -#ifndef OPENMW_COMPONENTS_RESOURCE_ARCHIVE_H -#define OPENMW_COMPONENTS_RESOURCE_ARCHIVE_H +#ifndef OPENMW_COMPONENTS_VFS_ARCHIVE_H +#define OPENMW_COMPONENTS_VFS_ARCHIVE_H -#include -#include +#include #include -#include +#include "filemap.hpp" namespace VFS { - - class File - { - public: - virtual ~File() = default; - - virtual Files::IStreamPtr open() = 0; - - virtual std::filesystem::path getPath() = 0; - }; - - using FileMap = std::map>; - class Archive { public: diff --git a/components/vfs/bsaarchive.hpp b/components/vfs/bsaarchive.hpp index 29098db45d..304fc438ad 100644 --- a/components/vfs/bsaarchive.hpp +++ b/components/vfs/bsaarchive.hpp @@ -2,6 +2,7 @@ #define VFS_BSAARCHIVE_HPP_ #include "archive.hpp" +#include "file.hpp" #include "pathutil.hpp" #include diff --git a/components/vfs/file.hpp b/components/vfs/file.hpp new file mode 100644 index 0000000000..f2dadb1162 --- /dev/null +++ b/components/vfs/file.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_COMPONENTS_VFS_FILE_H +#define OPENMW_COMPONENTS_VFS_FILE_H + +#include + +#include + +namespace VFS +{ + class File + { + public: + virtual ~File() = default; + + virtual Files::IStreamPtr open() = 0; + + virtual std::filesystem::path getPath() = 0; + }; +} + +#endif diff --git a/components/vfs/filemap.hpp b/components/vfs/filemap.hpp new file mode 100644 index 0000000000..1b7d390d88 --- /dev/null +++ b/components/vfs/filemap.hpp @@ -0,0 +1,19 @@ +#ifndef OPENMW_COMPONENTS_VFS_FILEMAP_H +#define OPENMW_COMPONENTS_VFS_FILEMAP_H + +#include +#include + +namespace VFS +{ + class File; + + namespace Path + { + class Normalized; + } + + using FileMap = std::map>; +} + +#endif diff --git a/components/vfs/filesystemarchive.hpp b/components/vfs/filesystemarchive.hpp index e31ef9bd30..00fe5ba971 100644 --- a/components/vfs/filesystemarchive.hpp +++ b/components/vfs/filesystemarchive.hpp @@ -2,8 +2,9 @@ #define OPENMW_COMPONENTS_RESOURCE_FILESYSTEMARCHIVE_H #include "archive.hpp" -#include +#include "file.hpp" +#include #include namespace VFS diff --git a/components/vfs/manager.cpp b/components/vfs/manager.cpp index cc231847f5..d312ce9d84 100644 --- a/components/vfs/manager.cpp +++ b/components/vfs/manager.cpp @@ -5,12 +5,19 @@ #include #include +#include #include "archive.hpp" +#include "file.hpp" #include "pathutil.hpp" +#include "recursivedirectoryiterator.hpp" namespace VFS { + Manager::Manager() = default; + + Manager::~Manager() = default; + void Manager::reset() { mIndex.clear(); @@ -70,13 +77,13 @@ namespace VFS return found->second->getPath(); } - Manager::RecursiveDirectoryRange Manager::getRecursiveDirectoryIterator(std::string_view path) const + RecursiveDirectoryRange Manager::getRecursiveDirectoryIterator(std::string_view path) const { if (path.empty()) return { mIndex.begin(), mIndex.end() }; std::string normalized = Path::normalizeFilename(path); const auto it = mIndex.lower_bound(normalized); - if (it == mIndex.end() || !it->first.starts_with(normalized)) + if (it == mIndex.end() || !it->first.view().starts_with(normalized)) return { it, it }; ++normalized.back(); return { it, mIndex.lower_bound(normalized) }; diff --git a/components/vfs/manager.hpp b/components/vfs/manager.hpp index 76405aae2c..05990a8607 100644 --- a/components/vfs/manager.hpp +++ b/components/vfs/manager.hpp @@ -4,32 +4,17 @@ #include #include -#include #include #include +#include #include -#include "archive.hpp" +#include "filemap.hpp" namespace VFS { - - template - class IteratorPair - { - public: - IteratorPair(Iterator first, Iterator last) - : mFirst(first) - , mLast(last) - { - } - Iterator begin() const { return mFirst; } - Iterator end() const { return mLast; } - - private: - Iterator mFirst; - Iterator mLast; - }; + class Archive; + class RecursiveDirectoryRange; /// @brief The main class responsible for loading files from a virtual file system. /// @par Various archive types (e.g. directories on the filesystem, or compressed archives) @@ -38,29 +23,11 @@ namespace VFS /// @par Most of the methods in this class are considered thread-safe, see each method documentation for details. class Manager { - class RecursiveDirectoryIterator - { - public: - RecursiveDirectoryIterator(FileMap::const_iterator it) - : mIt(it) - { - } - const std::string& operator*() const { return mIt->first; } - const std::string* operator->() const { return &mIt->first; } - bool operator!=(const RecursiveDirectoryIterator& other) { return mIt != other.mIt; } - RecursiveDirectoryIterator& operator++() - { - ++mIt; - return *this; - } - - private: - FileMap::const_iterator mIt; - }; - - using RecursiveDirectoryRange = IteratorPair; - public: + Manager(); + + ~Manager(); + // Empty the file index and unregister archives. void reset(); diff --git a/components/vfs/pathutil.hpp b/components/vfs/pathutil.hpp index 724b406f1d..9bcc263842 100644 --- a/components/vfs/pathutil.hpp +++ b/components/vfs/pathutil.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -51,6 +52,73 @@ namespace VFS::Path bool operator()(std::string_view left, std::string_view right) const { return pathLess(left, right); } }; + + class Normalized + { + public: + Normalized() = default; + + Normalized(std::string_view value) + : mValue(normalizeFilename(value)) + { + } + + Normalized(const char* value) + : Normalized(std::string_view(value)) + { + } + + Normalized(const std::string& value) + : Normalized(std::string_view(value)) + { + } + + explicit Normalized(std::string&& value) + : mValue(std::move(value)) + { + normalizeFilenameInPlace(mValue); + } + + const std::string& value() const& { return mValue; } + + std::string value() && { return std::move(mValue); } + + std::string_view view() const { return mValue; } + + operator std::string_view() const { return mValue; } + + operator const std::string&() const { return mValue; } + + friend bool operator==(const Normalized& lhs, const Normalized& rhs) = default; + + template + friend bool operator==(const Normalized& lhs, const T& rhs) + { + return lhs.mValue == rhs; + } + + friend bool operator<(const Normalized& lhs, const Normalized& rhs) { return lhs.mValue < rhs.mValue; } + + template + friend bool operator<(const Normalized& lhs, const T& rhs) + { + return lhs.mValue < rhs; + } + + template + friend bool operator<(const T& lhs, const Normalized& rhs) + { + return lhs < rhs.mValue; + } + + friend std::ostream& operator<<(std::ostream& stream, const Normalized& value) + { + return stream << value.mValue; + } + + private: + std::string mValue; + }; } #endif diff --git a/components/vfs/recursivedirectoryiterator.hpp b/components/vfs/recursivedirectoryiterator.hpp new file mode 100644 index 0000000000..39fb26e873 --- /dev/null +++ b/components/vfs/recursivedirectoryiterator.hpp @@ -0,0 +1,54 @@ +#ifndef OPENMW_COMPONENTS_VFS_RECURSIVEDIRECTORYITERATOR_H +#define OPENMW_COMPONENTS_VFS_RECURSIVEDIRECTORYITERATOR_H + +#include + +#include "filemap.hpp" +#include "pathutil.hpp" + +namespace VFS +{ + class RecursiveDirectoryIterator + { + public: + RecursiveDirectoryIterator(FileMap::const_iterator it) + : mIt(it) + { + } + + const std::string& operator*() const { return mIt->first.value(); } + + const std::string* operator->() const { return &mIt->first.value(); } + + RecursiveDirectoryIterator& operator++() + { + ++mIt; + return *this; + } + + friend bool operator==(const RecursiveDirectoryIterator& lhs, const RecursiveDirectoryIterator& rhs) = default; + + private: + FileMap::const_iterator mIt; + }; + + class RecursiveDirectoryRange + { + public: + RecursiveDirectoryRange(RecursiveDirectoryIterator first, RecursiveDirectoryIterator last) + : mBegin(first) + , mEnd(last) + { + } + + RecursiveDirectoryIterator begin() const { return mBegin; } + + RecursiveDirectoryIterator end() const { return mEnd; } + + private: + RecursiveDirectoryIterator mBegin; + RecursiveDirectoryIterator mEnd; + }; +} + +#endif diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 1343ac818c..83937a82a5 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -3,6 +3,7 @@ paths=( scripts/omw/activationhandlers.lua scripts/omw/ai.lua scripts/omw/input/playercontrols.lua + scripts/omw/mechanics/animationcontroller.lua scripts/omw/camera/camera.lua scripts/omw/mwui/init.lua scripts/omw/settings/player.lua diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 57460c4983..6da5d3d55a 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -68,6 +68,39 @@ You will find ``Morrowind.esm`` there. Users of other platforms running Wine, will find it at ``~/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind`` +Innoextract +^^^^^^^^^^^ + +macOS and Linux +~~~~~~~~~~~~~~~ + +If you have purchased "The Elder Scrolls III: Morrowind" `from GOG `_ and wish to extract the game files on a Linux system without using Wine, or on macOS, you can do so using `innoextract `_. First install innoextract. + +For Distributions Using `apt` (e.g., Ubuntu, Debian) +++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. code:: console + + sudo apt update + sudo apt install innoextract + +For macOS using Homebrew +++++++++++++++++++++++++ + +.. code:: console + + brew install innoextract + +Once innoextract is installed, download the game from GOG. The downloaded file should be called ``setup_tes_morrowind_goty_2.0.0.7.exe`` or something similar. When ``innoextract`` is run on it, it will extract the files directly into the folder the ``setup.exe`` file is located. If you have a specific folder where you want it to be extracted to, for example in ``~/Documents/Games/Morrowind`` You can specify it with the ``-d`` flag. + +.. code:: console + + $ innoextract ./setup_tes_morrowind_goty_2.0.0.7.exe -d ~/Documents/Games/Morrowind/ + +Assuming you used the filepath above, your ``.esm`` files will be located in ``~/Documents/Games/Morrowind/app/Data Files/``. + +You can now run the OpenMW launcher, and from there run the installation wizard. Point it to your ``Morrowind.esm`` in the folder you extracted it to, and follow the instructions. + ----- Steam ----- diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 857374b2b7..47807ab793 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -16,6 +16,7 @@ Lua API reference openmw_storage openmw_core openmw_types + openmw_animation openmw_async openmw_vfs openmw_world @@ -34,6 +35,7 @@ Lua API reference openmw_aux_ui interface_activation interface_ai + interface_animation interface_camera interface_controls interface_item_usage diff --git a/docs/source/reference/lua-scripting/interface_animation.rst b/docs/source/reference/lua-scripting/interface_animation.rst new file mode 100644 index 0000000000..5bde11775e --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_animation.rst @@ -0,0 +1,8 @@ +Interface AnimationController +============================= + +.. include:: version.rst + +.. raw:: html + :file: generated_html/scripts_omw_mechanics_animationcontroller.html + diff --git a/docs/source/reference/lua-scripting/openmw_animation.rst b/docs/source/reference/lua-scripting/openmw_animation.rst new file mode 100644 index 0000000000..35ac26ecec --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_animation.rst @@ -0,0 +1,7 @@ +Package openmw.animation +======================== + +.. include:: version.rst + +.. raw:: html + :file: generated_html/openmw_animation.html diff --git a/docs/source/reference/lua-scripting/tables/interfaces.rst b/docs/source/reference/lua-scripting/tables/interfaces.rst index e05eb642f0..5029baf0a3 100644 --- a/docs/source/reference/lua-scripting/tables/interfaces.rst +++ b/docs/source/reference/lua-scripting/tables/interfaces.rst @@ -10,6 +10,9 @@ * - :ref:`AI ` - by local scripts - Control basic AI of NPCs and creatures. + * - :ref:`AnimationController ` + - by local scripts + - Control animations of NPCs and creatures. * - :ref:`Camera ` - by player scripts - | Allows to alter behavior of the built-in camera script diff --git a/docs/source/reference/lua-scripting/tables/packages.rst b/docs/source/reference/lua-scripting/tables/packages.rst index 667b91ef63..247bd7eacc 100644 --- a/docs/source/reference/lua-scripting/tables/packages.rst +++ b/docs/source/reference/lua-scripting/tables/packages.rst @@ -13,6 +13,8 @@ +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.types ` | everywhere | | Functions for specific types of game objects. | +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw.animation ` | everywhere | | Animation controls | ++------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.async ` | everywhere | | Timers and callbacks. | +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.vfs ` | everywhere | | Read-only access to data directories via VFS. | diff --git a/docs/source/reference/modding/settings/GUI.rst b/docs/source/reference/modding/settings/GUI.rst index 76c83be4da..edacdc730a 100644 --- a/docs/source/reference/modding/settings/GUI.rst +++ b/docs/source/reference/modding/settings/GUI.rst @@ -11,7 +11,7 @@ scaling factor This setting scales GUI windows. A value of 1.0 results in the normal scale. Larger values are useful to increase the scale of the GUI for high resolution displays. -This setting can be configured in the Interface section of the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. font size --------- @@ -24,7 +24,7 @@ Allows to specify glyph size for in-game fonts. Note: default bitmap fonts are supposed to work with 16px size, otherwise glyphs will be blurry. TrueType fonts do not have this issue. -This setting can be configured in the Interface section of the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. menu transparency ----------------- @@ -65,7 +65,7 @@ The Bethesda provided assets have a 4:3 aspect ratio, but other assets are permi If this setting is false, the assets will be centered in the mentioned 4:3 aspect ratio, with black bars filling the remainder of the screen. -This setting can be configured in the Interface section of the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. subtitles --------- diff --git a/docs/source/reference/modding/settings/fog.rst b/docs/source/reference/modding/settings/fog.rst index b05112162e..20bfdaf14d 100644 --- a/docs/source/reference/modding/settings/fog.rst +++ b/docs/source/reference/modding/settings/fog.rst @@ -125,6 +125,8 @@ By default, the fog becomes thicker proportionally to your distance from the cli This setting makes the fog use the actual eye point distance (or so called Euclidean distance) to calculate the fog, which makes the fog look less artificial, especially if you have a wide FOV. Note that the rendering will act as if you have 'force shaders' option enabled with this on, which means that shaders will be used to render all objects and the terrain. +This setting can be controlled in the Settings tab of the launcher. + exponential fog --------------- @@ -135,6 +137,8 @@ exponential fog Similar to "radial fog" but uses an exponential formula for the fog. Note that the rendering will act as if you have 'force shaders' option enabled with this on, which means that shaders will be used to render all objects and the terrain. +This setting can be controlled in the Settings tab of the launcher. + sky blending ------------ @@ -146,6 +150,8 @@ Whether to use blending with the sky for everything that is close to the clippin If enabled the clipping plane becomes invisible. Note that the rendering will act as if you have 'force shaders' option enabled with this on, which means that shaders will be used to render all objects and the terrain. +This setting can be controlled in the Settings tab of the launcher. + sky blending start ------------------ @@ -155,6 +161,8 @@ sky blending start The fraction of the maximum distance at which blending with the sky starts. +This setting can be controlled in the Settings tab of the launcher. + sky rtt resolution ------------------ diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 31cc2703f2..368401f5c5 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -16,7 +16,7 @@ If the setting is 2, the crosshair is the colour of the colour crosshair owned s If the setting is 3, both the tool tip background and the crosshair are coloured. The crosshair is not visible if crosshair is false. -This setting can be configured in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. show projectile damage ---------------------- @@ -27,7 +27,7 @@ show projectile damage If this setting is true, the damage bonus of arrows and bolts will show on their tooltip. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. show melee info --------------- @@ -38,7 +38,7 @@ show melee info If this setting is true, the reach and speed of weapons will show on their tooltip. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. show enchant chance ------------------- @@ -49,7 +49,7 @@ show enchant chance Whether or not the chance of success will be displayed in the enchanting menu. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. best attack ----------- @@ -78,7 +78,7 @@ If this setting is false, player has to wait until end of death animation in all 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 be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. difficulty ---------- @@ -123,7 +123,7 @@ and the caster will absorb their own stat resulting in no effect on either the c This makes the gameplay as a mage easier, but these spells become imbalanced. This is how Morrowind behaves. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. classic calm spells behavior ---------------------------------------- @@ -137,7 +137,7 @@ This means that a Calm spell of any magnitude will always take actors out of com This is how Morrowind behaves without the Morrowind Code Patch. If this setting is off, Calm spells will only take their target out of combat once. Allowing them to re-engage if the spell was not sufficiently strong. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. use magic item animations ------------------------- @@ -161,7 +161,7 @@ show effect duration 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. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. enchanted weapons are magical ----------------------------- @@ -173,7 +173,7 @@ enchanted weapons are magical Make enchanted weapons without Magical flag bypass normal weapons resistance (and weakness) certain creatures have. This is how Morrowind behaves. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. prevent merchant equipping -------------------------- @@ -184,7 +184,7 @@ prevent merchant equipping Prevent merchants from equipping items that are sold to them. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. followers attack on sight ------------------------- @@ -196,7 +196,8 @@ followers attack on sight Make player followers and escorters start combat with enemies who have started combat with them or the player. Otherwise they wait for the enemies or the player to do an attack first. Please note this setting has not been extensively tested and could have side effects with certain quests. -This setting can be toggled in the Settings tab of the launcher. + +This setting can be controlled in the Settings tab of the launcher. shield sheathing ---------------- @@ -214,6 +215,8 @@ To avoid conflicts, you can use _sh mesh without "Bip01 Sheath" node for such "s Also you can use an _sh node with empty "Bip01 Sheath" node. In this case the engine will use basic shield model, but will use transformations from the "Bip01 Sheath" node. +This setting can be controlled in the Settings tab of the launcher. + weapon sheathing ---------------- @@ -226,6 +229,8 @@ If this setting is true, OpenMW will utilize weapon sheathing-compatible assets To make use of this, you need to have an xbase_anim_sh.nif file with weapon bones that will be injected into the skeleton. Additional _sh suffix models are not essential for weapon sheathing to work but will act as quivers or scabbards for the weapons they correspond to. +This setting can be controlled in the Settings tab of the launcher. + use additional anim sources --------------------------- @@ -238,7 +243,8 @@ For example, if the main animation mesh has name Meshes/x.nif, the engine will load all KF-files from Animations/x folder and its child folders. This can be useful if you want to use several animation replacers without merging them. Attention: animations from AnimKit have their own format and are not supposed to be directly loaded in-game! -This setting can only be configured by editing the settings configuration file. + +This setting can be controlled in the Settings tab of the launcher. barter disposition change is permanent -------------------------------------- @@ -251,7 +257,7 @@ If this setting is true, disposition change of merchants caused by trading will be permanent and won't be discarded upon exiting dialogue with them. This imitates the option that Morrowind Code Patch offers. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. only appropriate ammunition bypasses resistance ----------------------------------------------- @@ -264,7 +270,7 @@ If this setting is true, you will have to use the appropriate ammunition to bypa An enchanted bow with chitin arrows will no longer be enough for the purpose, while a steel longbow with glass arrows will still work. This was previously the default engine behavior that diverged from Morrowind design. -This setting can be toggled in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. strength influences hand to hand -------------------------------- @@ -454,6 +460,8 @@ Some mods add harvestable container models. When this setting is enabled, activa When this setting is turned off or when activating a regular container, the menu will open as usual. +This setting can be controlled in the Settings tab of the launcher. + allow actors to follow over water surface ----------------------------------------- @@ -489,6 +497,8 @@ day night switches Some mods add models which change visuals based on time of day. When this setting is enabled, supporting models will automatically make use of Day/night state. +This setting can be controlled in the Settings tab of the launcher. + unarmed creature attacks damage armor ------------------------------------- @@ -500,7 +510,7 @@ If disabled unarmed creature attacks do not reduce armor condition, just as with If enabled unarmed creature attacks reduce armor condition, the same as attacks from NPCs and armed creatures. -This setting can be controlled in the Settings tab of the launcher, under Game Mechanics. +This setting can be controlled in the Settings tab of the launcher. actor collision shape type -------------------------- @@ -518,6 +528,8 @@ will not be useful with another. * 1: Rotating box * 2: Cylinder +This setting can be controlled in the Settings tab of the launcher. + player movement ignores animation --------------------------------- @@ -528,4 +540,4 @@ player movement ignores animation In third person, the camera will sway along with the movement animations of the player. Enabling this option disables this swaying by having the player character move independently of its animation. -This setting can be controlled in the Settings tab of the launcher, under Visuals. +This setting can be controlled in the Settings tab of the launcher. diff --git a/docs/source/reference/modding/settings/general.rst b/docs/source/reference/modding/settings/general.rst index e56b84ab89..d7afc04349 100644 --- a/docs/source/reference/modding/settings/general.rst +++ b/docs/source/reference/modding/settings/general.rst @@ -29,7 +29,7 @@ Specify the format for screen shots taken by pressing the screen shot key (bound This setting should be the file extension commonly associated with the desired format. The formats supported will be determined at compilation, but "jpg", "png", and "tga" should be allowed. -This setting can be configured in the Settings tab of the launcher. +This setting can be controlled in the Settings tab of the launcher. texture mag filter ------------------ @@ -69,6 +69,8 @@ notify on saved screenshot Show message box when screenshot is saved to a file. +This setting can be controlled in the Settings tab of the launcher. + preferred locales ----------------- diff --git a/docs/source/reference/modding/settings/shadows.rst b/docs/source/reference/modding/settings/shadows.rst index 0670a81092..c7f7958edd 100644 --- a/docs/source/reference/modding/settings/shadows.rst +++ b/docs/source/reference/modding/settings/shadows.rst @@ -16,6 +16,8 @@ Unlike in the original Morrowind engine, 'Shadow Mapping' is used, which can hav Bear in mind that this will force OpenMW to use shaders as if :ref:`force shaders` was enabled. A keen developer may be able to implement compatibility with fixed-function mode using the advice of `this post `_, but it may be more difficult than it seems. +This setting can be controlled in the Settings tab of the launcher. + number of shadow maps --------------------- @@ -38,6 +40,8 @@ The maximum distance from the camera shadows cover, limiting their overall area and improving their quality and performance at the cost of removing shadows of distant objects or terrain. Set this to a non-positive value to remove the limit. +This setting can be controlled in the Settings tab of the launcher. + shadow fade start ------------------- @@ -49,6 +53,8 @@ The fraction of the maximum shadow map distance at which the shadows will begin Tweaking it will make the transition proportionally more or less smooth. This setting has no effect if the maximum shadow map distance is non-positive (infinite). +This setting can be controlled in the Settings tab of the launcher. + allow shadow map overlap ------------------------ @@ -90,6 +96,8 @@ compute scene bounds Two different ways to make better use of shadow map(s) by making them cover a smaller area. While primitives give better shadows at expense of more CPU, bounds gives better performance overall but with lower quality shadows. There is also the ability to disable this computation with none. +This setting can be controlled in the Settings tab of the launcher. + shadow map resolution --------------------- @@ -101,6 +109,8 @@ Control How large to make the shadow map(s). Higher values increase GPU load but can produce better-looking results. Power-of-two values may turn out to be faster than smaller values which are not powers of two on some GPU/driver combinations. +This setting can be controlled in the Settings tab of the launcher. + actor shadows ------------- @@ -111,6 +121,8 @@ actor shadows Allow actors to cast shadows. Potentially decreases performance. +This setting can be controlled in the Settings tab of the launcher. + player shadows -------------- @@ -121,6 +133,8 @@ player shadows Allow the player to cast shadows. Potentially decreases performance. +This setting can be controlled in the Settings tab of the launcher. + terrain shadows --------------- @@ -131,6 +145,8 @@ terrain shadows Allow terrain to cast shadows. Potentially decreases performance. +This setting can be controlled in the Settings tab of the launcher. + object shadows -------------- @@ -141,6 +157,8 @@ object shadows Allow static objects to cast shadows. Potentially decreases performance. +This setting can be controlled in the Settings tab of the launcher. + enable indoor shadows --------------------- @@ -152,6 +170,8 @@ Allow shadows indoors. Due to limitations with Morrowind's data, only actors can cast shadows indoors without the ceiling casting a shadow everywhere. Some might feel this is distracting as shadows can be cast through other objects, so indoor shadows can be disabled completely. +This setting can be controlled in the Settings tab of the launcher. + Expert settings *************** diff --git a/docs/source/reference/modding/settings/sound.rst b/docs/source/reference/modding/settings/sound.rst index 4cc665582b..7a5718735c 100644 --- a/docs/source/reference/modding/settings/sound.rst +++ b/docs/source/reference/modding/settings/sound.rst @@ -13,7 +13,7 @@ which should usually be sufficient, but if you need to explicitly specify a devi The names of detected devices can be found in the openmw.log file in your configuration directory. -This setting can be configured by editing the settings configuration file, or in the Audio tab of the OpenMW Launcher. +This setting can be controlled in the Settings tab of the launcher. master volume ------------- @@ -111,7 +111,8 @@ Enabling HRTF may also require an OpenAL Soft version greater than 1.17.0, and possibly some operating system configuration. A value of 0 disables HRTF processing, while a value of 1 explicitly enables HRTF processing. The default value is -1, which should enable the feature automatically for most users when possible. -This setting can be configured by editing the settings configuration file, or in the Audio tab of the OpenMW Launcher. + +This setting can be controlled in the Settings tab of the launcher. hrtf ---- @@ -123,6 +124,6 @@ hrtf This setting specifies which HRTF profile to use when HRTF is enabled. Blank means use the default. This setting has no effect if HRTF is not enabled based on the hrtf enable setting. Allowed values for this field are enumerated in openmw.log file is an HRTF enabled audio system is installed. - The default value is empty, which uses the default profile. -This setting can be configured by editing the settings configuration file, or in the Audio tab of the OpenMW Launcher. + +This setting can be controlled in the Settings tab of the launcher. diff --git a/docs/source/reference/modding/settings/video.rst b/docs/source/reference/modding/settings/video.rst index 801cf63d5b..46016247ff 100644 --- a/docs/source/reference/modding/settings/video.rst +++ b/docs/source/reference/modding/settings/video.rst @@ -13,8 +13,8 @@ Larger values produce more detailed images within the constraints of your graphi but may reduce the frame rate. The window resolution can be selected from a menu of common screen sizes -in the Video tab of the Video Panel of the Options menu, or in the Graphics tab of the OpenMW Launcher. -The horizontal resolution can also be set to a custom value in the Graphics tab of the OpenMW Launcher. +in the Video tab of the Video Panel of the Options menu, or in the Display tab of the launcher. +The horizontal resolution can also be set to a custom value in the Display tab of the launcher. resolution y ------------ @@ -28,8 +28,8 @@ Larger values produce more detailed images within the constraints of your graphi but may reduce the frame rate. The window resolution can be selected from a menu of common screen sizes -in the Video tab of the Video Panel of the Options menu, or in the Graphics tab of the OpenMW Launcher. -The vertical resolution can also be set to a custom value in the Graphics tab of the OpenMW Launcher. +in the Video tab of the Video Panel of the Options menu, or in the Display tab of the launcher. +The vertical resolution can also be set to a custom value in the Display tab of the launcher. window mode ----------- @@ -48,7 +48,7 @@ This setting determines the window mode. This setting can be toggled in game using the dropdown list in the Video tab of the Video panel in the Options menu. -It can also be toggled with the window mode dropdown in the Graphics tab of the OpenMW Launcher. +It can also be toggled with the window mode dropdown in the Display tab of the launcher. screen ------ @@ -63,7 +63,7 @@ since this is the only way to control which screen is used, but it can also be used to control which screen a normal window or a borderless window opens on as well. The screens are numbered in increasing order, beginning with 0. -This setting can be selected from a pull down menu in the Graphics tab of the OpenMW Launcher, +This setting can be selected from a pull down menu in the Display tab of the OpenMW Launcher, but cannot be changed during game play. minimize on focus loss @@ -143,7 +143,7 @@ cannot reach your display's refresh rate. This prevents the input lag from becom Some hardware might not support this mode, in which case traditional vsync will be used. This setting can be adjusted in game using the VSync combo box in the Video tab of the Video panel in the Options menu. -It can also be changed by toggling the Vertical Sync combo box in the Graphics tab of the OpenMW Launcher. +It can also be changed by toggling the Vertical Sync combo box in the Display tab of the launcher. framerate limit --------------- diff --git a/docs/source/reference/postprocessing/omwfx.rst b/docs/source/reference/postprocessing/omwfx.rst index 7a7cdc198b..b47e509925 100644 --- a/docs/source/reference/postprocessing/omwfx.rst +++ b/docs/source/reference/postprocessing/omwfx.rst @@ -561,7 +561,7 @@ color buffer will accumulate. source_format = rgb; internal_format = rgb16f; source_type = float; - clear_color = vec4(1,0,0,1); + clear_color = vec4(0,0,0,1); } fragment red(target=RT_Red,blend=(add, src_color, one), rt1=RT_Red) { diff --git a/extern/osg-ffmpeg-videoplayer/videostate.cpp b/extern/osg-ffmpeg-videoplayer/videostate.cpp index 096651dfd8..c062c99b65 100644 --- a/extern/osg-ffmpeg-videoplayer/videostate.cpp +++ b/extern/osg-ffmpeg-videoplayer/videostate.cpp @@ -598,8 +598,17 @@ public: if(av_read_frame(pFormatCtx, packet.get()) < 0) { - if (self->audioq.nb_packets == 0 && self->videoq.nb_packets == 0 && self->pictq_size == 0) - self->mVideoEnded = true; + if (self->audioq.nb_packets == 0 && self->videoq.nb_packets == 0) + { + self->pictq_mutex.lock(); + bool videoEnded = self->pictq_size == 0; + self->pictq_mutex.unlock(); + if (videoEnded) + self->mVideoEnded = true; + else + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + continue; } else diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 8027cfb6e2..055154f555 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -76,6 +76,7 @@ set(BUILTIN_DATA_FILES scripts/omw/console/local.lua scripts/omw/console/player.lua scripts/omw/console/menu.lua + scripts/omw/mechanics/animationcontroller.lua scripts/omw/mechanics/playercontroller.lua scripts/omw/settings/menu.lua scripts/omw/settings/player.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 6d47b96e0a..2ec3e2c7ef 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -11,6 +11,7 @@ GLOBAL: scripts/omw/activationhandlers.lua GLOBAL: scripts/omw/cellhandlers.lua GLOBAL: scripts/omw/usehandlers.lua GLOBAL: scripts/omw/worldeventhandlers.lua +CREATURE, NPC, PLAYER: scripts/omw/mechanics/animationcontroller.lua PLAYER: scripts/omw/mechanics/playercontroller.lua MENU: scripts/omw/camera/settings.lua PLAYER: scripts/omw/camera/camera.lua diff --git a/files/data/l10n/OMWEngine/de.yaml b/files/data/l10n/OMWEngine/de.yaml index 0f729d0077..2874001309 100644 --- a/files/data/l10n/OMWEngine/de.yaml +++ b/files/data/l10n/OMWEngine/de.yaml @@ -87,6 +87,10 @@ LightsBoundingSphereMultiplier: "Bounding-Sphere-Multiplikator" LightsBoundingSphereMultiplierTooltip: "Standard: 1.65\nMultiplikator für Begrenzungskugel.\nHöhere Zahlen ermöglichen einen sanften Abfall, erfordern jedoch eine Erhöhung der Anzahl der maximalen Lichter.\n\nBeeinflusst nicht die Beleuchtung oder Lichtstärke." LightsFadeStartMultiplier: "Licht Verblassungs-Start-Multiplikator" LightsFadeStartMultiplierTooltip: "Standard: 0.85\nBruchteil der maximalen Entfernung, bei der die Lichter zu verblassen beginnen.\n\nStellen Sie hier einen niedrigen Wert für langsamere Übergänge oder einen hohen Wert für schnellere Übergänge ein." +#LightsLightingMethodTooltip: "Set the internal handling of light sources.\n\n +# \"Legacy\" always uses 8 lights per object and provides a lighting closest to an original game.\n\n +# \"Shaders (compatibility)\" removes the 8 light limit. This mode also enables lighting on groundcover and a configurable light fade. It is recommended to use this with older hardware and a light limit closer to 8.\n\n +# \"Shaders\" carries all of the benefits that \"Shaders (compatibility)\" does, but uses a modern approach that allows for a higher max lights count with little to no performance penalties on modern hardware." LightsMaximumDistance: "Maximale Lichtreichweite" LightsMaximumDistanceTooltip: "Standard: 8192\nMaximale Entfernung, bei der Lichter erscheinen (gemessen in Einheiten).\n\nSetzen Sie dies auf 0, um eine unbegrenzte Entfernung zu verwenden." LightsMinimumInteriorBrightness: "Minimale Innenhelligkeit" diff --git a/files/data/l10n/OMWEngine/en.yaml b/files/data/l10n/OMWEngine/en.yaml index 09db2b496d..0455d11e07 100644 --- a/files/data/l10n/OMWEngine/en.yaml +++ b/files/data/l10n/OMWEngine/en.yaml @@ -107,6 +107,10 @@ LightsBoundingSphereMultiplier: "Bounding Sphere Multiplier" LightsBoundingSphereMultiplierTooltip: "Default: 1.65\nMultipler for bounding sphere of lights.\nHigher numbers allows for smooth falloff but require an increase in number of max lights.\n\nDoes not effect the illumination or strength of lights." LightsFadeStartMultiplier: "Fade Start Multiplier" LightsFadeStartMultiplierTooltip: "Default: 0.85\nFraction of maximum distance at which lights will start to fade.\n\nSet this to a low value for slower transitions or a high value for quicker transitions." +LightsLightingMethodTooltip: "Set the internal handling of light sources.\n\n + \"Legacy\" always uses 8 lights per object and provides a lighting closest to an original game.\n\n + \"Shaders (compatibility)\" removes the 8 light limit. This mode also enables lighting on groundcover and a configurable light fade. It is recommended to use this with older hardware and a light limit closer to 8.\n\n + \"Shaders\" carries all of the benefits that \"Shaders (compatibility)\" does, but uses a modern approach that allows for a higher max lights count with little to no performance penalties on modern hardware." LightsMaximumDistance: "Maximum Light Distance" LightsMaximumDistanceTooltip: "Default: 8192\nMaximum distance at which lights will appear (measured in units).\n\nSet this to 0 to use an unlimited distance." LightsMinimumInteriorBrightness: "Minimum Interior Brightness" diff --git a/files/data/l10n/OMWEngine/fr.yaml b/files/data/l10n/OMWEngine/fr.yaml index f2772b017e..85bac08612 100644 --- a/files/data/l10n/OMWEngine/fr.yaml +++ b/files/data/l10n/OMWEngine/fr.yaml @@ -107,6 +107,10 @@ LightsBoundingSphereMultiplier: "Multiplicateur de sphère englobante" LightsBoundingSphereMultiplierTooltip: "valeur par défaut: 1.65\nMultiplicateur pour le rayon de la sphère incluant les sources lumineuses.\nUn multiplicateur plus élevé permet une extinction plus douce, mais applique un plus grand nombre de sources lumineuses sur chaque objet.\n\nCe paramètre ne modifie ni l'intensité ni la luminance des lumières." LightsFadeStartMultiplier: "Seuil de perte d'éclat lumineux" LightsFadeStartMultiplierTooltip: "valeur par défaut: 0.85\nFraction de la distance maximale d'une source à partir de laquelle l'intensité lumineuse commence à décroître.\n\nSélectionnez une valeur basse pour une transition douce ou une valeur plus élevée pour une transition plus abrupte." +#LightsLightingMethodTooltip: "Set the internal handling of light sources.\n\n +# \"Legacy\" always uses 8 lights per object and provides a lighting closest to an original game.\n\n +# \"Shaders (compatibility)\" removes the 8 light limit. This mode also enables lighting on groundcover and a configurable light fade. It is recommended to use this with older hardware and a light limit closer to 8.\n\n +# \"Shaders\" carries all of the benefits that \"Shaders (compatibility)\" does, but uses a modern approach that allows for a higher max lights count with little to no performance penalties on modern hardware." LightsMaximumDistance: "Distance maximale des sources lumineuses" LightsMaximumDistanceTooltip: "valeur par défaut: 8192\nDistance maximale d'affichage des sources lumineuses (en unité de distance).\n\nMettez cette valeur à 0 pour une distance d'affichage infinie." LightsMinimumInteriorBrightness: "Luminosité intérieure minimale" diff --git a/files/data/l10n/OMWEngine/ru.yaml b/files/data/l10n/OMWEngine/ru.yaml index 2bcb76a442..8d221fe33c 100644 --- a/files/data/l10n/OMWEngine/ru.yaml +++ b/files/data/l10n/OMWEngine/ru.yaml @@ -107,6 +107,10 @@ LightsBoundingSphereMultiplier: "Множитель размера ограни LightsBoundingSphereMultiplierTooltip: "Значение по умолчанию: 1.65\nМножитель размера ограничивающей сферы источников света.\nВысокие значения делают затухание света плавнее, но требуют более высокого максимального количества источников света.\n\nНастройка не влияет на уровень освещения или мощность источников света." LightsFadeStartMultiplier: "Множитель начала затухания" LightsFadeStartMultiplierTooltip: "Значение по умолчанию: 0.85\nДоля расстояния (относительно дальности отображения источников света), на которой свет начинает затухать.\n\nНизкие значения ведут к плавному затуханию, высокие - к резкому." +LightsLightingMethodTooltip: "Задает способ обработки источников света.\n\n + \"Устаревший\" всегда использует 8 источников света на объект и выдает освещение, наиболее близкое к таковому в оригинальной игре.\n\n + \"Шейдеры (режим совместимости)\" убирает ограничение в 8 источников света. Этот режим также позволяет освещению влиять на анимированную траву и позволяет настроить угасание света на расстоянии. Рекомендуется использовать этот режим на устаревшем аппаратном обеспечении и с количеством источников света на объект около 8.\n\n + \"Шейдеры\" работает аналогично режиму \"Шейдеры (режим совместимости)\", но использует более современный подход, позволяющий использовать большее количество источников света с минимальным влиянием на производительность на современном аппаратном обеспечении." LightsMaximumDistance: "Дальность отображения источников света" LightsMaximumDistanceTooltip: "Значение по умолчанию: 8192\nМаксимальное расстояние, на котором будут отображаться источники света (во внутриигровых единицах измерения).\n\nЕсли 0, то расстояние не ограничено." LightsMinimumInteriorBrightness: "Минимальный уровень освещения в помещениях" diff --git a/files/data/l10n/OMWEngine/sv.yaml b/files/data/l10n/OMWEngine/sv.yaml index dc65726fdd..134fab0e95 100644 --- a/files/data/l10n/OMWEngine/sv.yaml +++ b/files/data/l10n/OMWEngine/sv.yaml @@ -107,6 +107,10 @@ LightsBoundingSphereMultiplier: "Gränssfärsmultiplikator" LightsBoundingSphereMultiplierTooltip: "Förvalt: 1.65\nMultiplikator för ljusens gränssfär.\nHögre värden ger mjukare minskning av gränssfären, men kräver högre värde i Max antal ljuskällor.\n\nPåverkar inte ljusstyrkan." LightsFadeStartMultiplier: "Blekningsstartmultiplikator" LightsFadeStartMultiplierTooltip: "Förvalt: 0.85\nFraktion av det maximala avståndet från vilket ljuskällor börjar blekna.\n\nVälj lågt värde för långsammare övergång eller högre värde för snabbare övergång." +#LightsLightingMethodTooltip: "Set the internal handling of light sources.\n\n +# \"Legacy\" always uses 8 lights per object and provides a lighting closest to an original game.\n\n +# \"Shaders (compatibility)\" removes the 8 light limit. This mode also enables lighting on groundcover and a configurable light fade. It is recommended to use this with older hardware and a light limit closer to 8.\n\n +# \"Shaders\" carries all of the benefits that \"Shaders (compatibility)\" does, but uses a modern approach that allows for a higher max lights count with little to no performance penalties on modern hardware." LightsMaximumDistance: "Maximalt ljusavstånd" LightsMaximumDistanceTooltip: "Förvalt: 8192\nMaximala avståndet där ljuskällor syns (mätt i enheter).\n\nVärdet 0 ger oändligt avstånd." LightsMinimumInteriorBrightness: "Minsta ljusstyrka i interiörer" diff --git a/files/data/mygui/openmw_settings_window.layout b/files/data/mygui/openmw_settings_window.layout index e912ababfd..27298b9756 100644 --- a/files/data/mygui/openmw_settings_window.layout +++ b/files/data/mygui/openmw_settings_window.layout @@ -536,6 +536,9 @@ + + + @@ -561,6 +564,10 @@ + + + + @@ -570,7 +577,7 @@ - + diff --git a/files/data/scripts/omw/mechanics/animationcontroller.lua b/files/data/scripts/omw/mechanics/animationcontroller.lua new file mode 100644 index 0000000000..3293668387 --- /dev/null +++ b/files/data/scripts/omw/mechanics/animationcontroller.lua @@ -0,0 +1,145 @@ +local anim = require('openmw.animation') +local self = require('openmw.self') + +local playBlendedHandlers = {} +local function onPlayBlendedAnimation(groupname, options) + for i = #playBlendedHandlers, 1, -1 do + if playBlendedHandlers[i](groupname, options) == false then + return + end + end +end + +local function playBlendedAnimation(groupname, options) + onPlayBlendedAnimation(groupname, options) + if options.skip then + return + end + anim.playBlended(self, groupname, options) +end + +local textKeyHandlers = {} +local function onAnimationTextKey(groupname, key) + local handlers = textKeyHandlers[groupname] + if handlers then + for i = #handlers, 1, -1 do + if handlers[i](groupname, key) == false then + return + end + end + end + handlers = textKeyHandlers[''] + if handlers then + for i = #handlers, 1, -1 do + if handlers[i](groupname, key) == false then + return + end + end + end +end + +local initialized = false + +local function onUpdate(dt) + -- The script is loaded before the actor's CharacterController object is initialized, therefore + -- we have to delay this initialization step or the call won't have any effect. + if not initialized then + self:_enableLuaAnimations(true) + initialized = true + end +end + +return { + engineHandlers = { + _onPlayAnimation = playBlendedAnimation, + _onAnimationTextKey = onAnimationTextKey, + onUpdate = onUpdate, + }, + + interfaceName = 'AnimationController', + --- + -- Animation controller interface + -- @module AnimationController + -- @usage local anim = require('openmw.animation') + -- local I = require('openmw.interfaces') + -- + -- -- play spellcast animation + -- I.AnimationController.playBlendedAnimation('spellcast', { startkey = 'self start', stopkey = 'self stop', priority = { + -- [anim.BONE_GROUP.RightArm] = anim.PRIORITY.Weapon, + -- [anim.BONE_GROUP.LeftArm] = anim.PRIORITY.Weapon, + -- [anim.BONE_GROUP.Torso] = anim.PRIORITY.Weapon, + -- [anim.BONE_GROUP.LowerBody] = anim.PRIORITY.WeaponLowerBody + -- } }) + -- + -- @usage -- react to the spellcast release textkey + -- I.AnimationController.addTextKeyHandler('spellcast', function(groupname, key) + -- -- Note, Lua is 1-indexed so have to subtract 1 less than the length of 'release' + -- if key.sub(key, #key - 6) == 'release' then + -- print('Abra kadabra!') + -- end + -- end) + -- + -- @usage -- Add a text key handler that will react to all keys + -- I.AnimationController.addTextKeyHandler('', function(groupname, key) + -- if key.sub(key, #key - 2) == 'hit' and not key.sub(key, #key - 7) == ' min hit' then + -- print('Hit!') + -- end + -- end) + -- + -- @usage -- Make a handler that changes player attack speed based on current fatigue + -- I.AnimationController.addPlayBlendedAnimationHandler(function (groupname, options) + -- local stop = options.stopkey + -- if #stop > 10 and stop.sub(stop, #stop - 10) == ' max attack' then + -- -- This is an attack wind up animation, scale its speed by attack + -- local fatigue = Actor.stats.dynamic.fatigue(self) + -- local factor = 1 - fatigue.current / fatigue.base + -- speed = 1 - factor * 0.8 + -- options.speed = speed + -- end + -- end) + -- + + interface = { + --- Interface version + -- @field [parent=#AnimationController] #number version + version = 0, + + --- AnimationController Package + -- @type Package + + --- Make this actor play an animation. Makes a call to @{openmw.animation#playBlended}, after invoking handlers added through addPlayBlendedAnimationHandler + -- @function [parent=#AnimationController] playBlendedAnimation + -- @param #string groupname The animation group to be played + -- @param #table options The table of play options that will be passed to @{openmw.animation#playBlended} + playBlendedAnimation = playBlendedAnimation, + + --- Add new playBlendedAnimation handler for this actor + -- If `handler(groupname, options)` returns false, other handlers for + -- the call will be skipped. + -- @function [parent=#AnimationController] addPlayBlendedAnimationHandler + -- @param #function handler The handler. + addPlayBlendedAnimationHandler = function(handler) + playBlendedHandlers[#playBlendedHandlers + 1] = handler + end, + + --- Add new text key handler for this actor + -- While playing, some animations emit text key events. Register a handle to listen for all + -- text key events associated with this actor's animations. + -- If `handler(groupname, key)` returns false, other handlers for + -- the call will be skipped. + -- @function [parent=#AnimationController] addTextKeyHandler + -- @param #string groupname Name of the animation group to listen to keys for. If the empty string or nil, all keys will be received + -- @param #function handler The handler. + addTextKeyHandler = function(groupname, handler) + if not groupname then + groupname = "" + end + local handlers = textKeyHandlers[groupname] + if handlers == nil then + handlers = {} + textKeyHandlers[groupname] = handlers + end + handlers[#handlers + 1] = handler + end, + } +} \ No newline at end of file diff --git a/files/lua_api/openmw/ambient.lua b/files/lua_api/openmw/ambient.lua index 9601ecc076..c10e50ff4a 100644 --- a/files/lua_api/openmw/ambient.lua +++ b/files/lua_api/openmw/ambient.lua @@ -12,7 +12,7 @@ -- @param #string soundId ID of Sound record to play -- @param #table options An optional table with additional optional arguments. Can contain: -- --- * `timeOffset` - a floating point number >= 0, to some time (in second) from beginning of sound file (default: 0); +-- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from beginning of sound file (default: 0); -- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); -- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); -- * `scale` - a boolean, to set if sound pitch should be scaled by simulation time scaling (default: true); @@ -32,7 +32,7 @@ -- @param #string fileName Path to sound file in VFS -- @param #table options An optional table with additional optional arguments. Can contain: -- --- * `timeOffset` - a floating point number >= 0, to some time (in second) from beginning of sound file (default: 0); +-- * `timeOffset` - a floating point number >= 0, to skip some time (in seconds) from beginning of sound file (default: 0); -- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); -- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); -- * `scale` - a boolean, to set if sound pitch should be scaled by simulation time scaling (default: true); @@ -76,7 +76,13 @@ -- Play a sound file as a music track -- @function [parent=#ambient] streamMusic -- @param #string fileName Path to file in VFS --- @usage ambient.streamMusic("Music\\Test\\Test.mp3"); +-- @param #table options An optional table with additional optional arguments. Can contain: +-- +-- * `fadeOut` - a floating point number >= 0, time (in seconds) to fade out current track before playing this one (default 1.0); +-- @usage local params = { +-- fadeOut=2.0 +-- }; +-- ambient.streamMusic("Music\\Test\\Test.mp3", params) --- -- Stop to play current music diff --git a/files/lua_api/openmw/animation.lua b/files/lua_api/openmw/animation.lua new file mode 100644 index 0000000000..bb5a0594df --- /dev/null +++ b/files/lua_api/openmw/animation.lua @@ -0,0 +1,255 @@ +--- +-- `openmw.animation` defines functions that allow control of character animations +-- Note that for some methods, such as @{openmw.animation#playBlended} you should use the associated methods on the +-- [AnimationController](interface_animation.html) interface rather than invoking this API directly. +-- @module animation +-- @usage local anim = require('openmw.animation') + +--- Possible @{#Priority} values +-- @field [parent=#animation] #Priority PRIORITY + +--- `animation.PRIORITY` +-- @type Priority +-- @field #number Default "0" +-- @field #number WeaponLowerBody "1" +-- @field #number SneakIdleLowerBody "2" +-- @field #number SwimIdle "3" +-- @field #number Jump "4" +-- @field #number Movement "5" +-- @field #number Hit "6" +-- @field #number Weapon "7" +-- @field #number Block "8" +-- @field #number Knockdown "9" +-- @field #number Torch "10" +-- @field #number Storm "11" +-- @field #number Death "12" +-- @field #number Scripted "13" Special priority used by scripted animations. When any animation with this priority is present, all animations without this priority are paused. + +--- Possible @{#BlendMask} values +-- @field [parent=#animation] #BlendMask BLEND_MASK + +--- `animation.BLEND_MASK` +-- @type BlendMask +-- @field #number LowerBody "1" All bones from 'Bip01 pelvis' and below +-- @field #number Torso "2" All bones from 'Bip01 Spine1' and up, excluding arms +-- @field #number LeftArm "4" All bones from 'Bip01 L Clavicle' and out +-- @field #number RightArm "8" All bones from 'Bip01 R Clavicle' and out +-- @field #number UpperBody "14" All bones from 'Bip01 Spine1' and up, including arms +-- @field #number All "15" All bones + +--- Possible @{#BoneGroup} values +-- @field [parent=#animation] #BoneGroup BONE_GROUP + +--- `animation.BONE_GROUP` +-- @type BoneGroup +-- @field #number LowerBody "1" All bones from 'Bip01 pelvis' and below +-- @field #number Torso "2" All bones from 'Bip01 Spine1' and up, excluding arms +-- @field #number LeftArm "3" All bones from 'Bip01 L Clavicle' and out +-- @field #number RightArm "4" All bones from 'Bip01 R Clavicle' and out + + +--- +-- Check if the object has an animation object or not +-- @function [parent=#animation] hasAnimation +-- @param openmw.core#GameObject actor +-- @return #boolean + +--- +-- Skips animations for one frame, equivalent to mwscript's SkipAnim +-- Can be used only in local scripts on self. +-- @function [parent=#animation] skipAnimationThisFrame +-- @param openmw.core#GameObject actor + +--- +-- Get the absolute position within the animation track of the given text key +-- @function [parent=#animation] getTextKeyTime +-- @param openmw.core#GameObject actor +-- @param #string text key +-- @return #number + +--- +-- Check if the given animation group is currently playing +-- @function [parent=#animation] isPlaying +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #boolean + +--- +-- Get the current absolute time of the given animation group if it is playing, or -1 if it is not playing. +-- @function [parent=#animation] getCurrentTime +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #number + +--- +-- Check whether the animation is a looping animation or not. This is determined by a combination +-- of groupname, some of which are hardcoded to be looping, and the presence of loop start/stop keys. +-- The groupnames that are hardcoded as looping are the following, as well as per-weapon-type suffixed variants of each. +-- "walkforward", "walkback", "walkleft", "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", +-- "runforward", "runback", "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", +-- "sneakforward", "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright", +-- "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", "idle8", +-- "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", "inventoryweapononehand", +-- "inventoryweapontwohand", "inventoryweapontwowide" +-- @function [parent=#animation] isLoopingAnimation +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #boolean + + +--- +-- Cancels and removes the animation group from the list of active animations +-- Can be used only in local scripts on self. +-- @function [parent=#animation] cancel +-- @param openmw.core#GameObject actor +-- @param #string groupname + +--- +-- Enables or disables looping for the given animation group. Looping is enabled by default. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] setLoopingEnabled +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @param #boolean enabled + +--- +-- Returns the completion of the animation, or nil if the animation group is not active. +-- @function [parent=#animation] getCompletion +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #number, #nil + +--- +-- Returns the remaining number of loops, not counting the current loop, or nil if the animation group is not active. +-- @function [parent=#animation] getLoopCount +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #number, #nil + +--- +-- Get the current playback speed of an animation group, or nil if the animation group is not active. +-- @function [parent=#animation] getSpeed +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #number, #nil + +--- +-- Modifies the playback speed of an animation group. +-- Note that this is not sticky and only affects the speed until the currently playing sequence ends. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] setSpeed +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @param #number speed The new animation speed, where speed=1 is normal speed. + +--- +-- Clears all animations currently in the animation queue. This affects animations played by mwscript, @{openmw.animation#playQueued}, and ai packages, but does not affect animations played using @{openmw.animation#playBlended}. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] clearAnimationQueue +-- @param openmw.core#GameObject actor +-- @param #boolean clearScripted whether to keep animation with priority Scripted or not. + +--- +-- Acts as a slightly extended version of MWScript's LoopGroup. Plays this animation exclusively +-- until it ends, or the queue is cleared using #clearAnimationQueue. Use #clearAnimationQueue and the `startkey` option +-- to imitate the behavior of LoopGroup's play modes. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] playQueued +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @param #table options A table of play options. Can contain: +-- +-- * `loops` - a number >= 0, the number of times the animation should loop after the first play (default: infinite). +-- * `speed` - a floating point number >= 0, the speed at which the animation should play (default: 1); +-- * `startkey` - the animation key at which the animation should start (default: "start") +-- * `stopkey` - the animation key at which the animation should end (default: "stop") +-- * `forceloop` - a boolean, to set if the animation should loop even if it's not a looping animation (default: false) +-- +-- @usage -- Play death1 without waiting. Equivalent to playgroup, death1, 1 +-- anim.clearAnimationQueue(self, false) +-- anim.playQueued(self, 'death1') +-- +-- @usage -- Play an animation group with custom start/stop keys +-- anim.clearAnimationQueue(self, false) +-- anim.playQueued(self, 'spellcast', { startkey = 'self start', stopkey = 'self stop' }) +-- + +--- +-- Play an animation directly. You probably want to use the [AnimationController](interface_animation.html) interface, which will trigger relevant handlers, +-- instead of calling this directly. Note that the still hardcoded character controller may at any time and for any reason alter +-- or cancel currently playing animations, so making your own calls to this function either directly or through the [AnimationController](interface_animation.html) +-- interface may be of limited utility. For now, use openmw.animation#playQueued to script your own animations. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] playBlended +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @param #table options A table of play options. Can contain: +-- +-- * `loops` - a number >= 0, the number of times the animation should loop after the first play (default: 0). +-- * `priority` - Either a single #Priority value that will be assigned to all bone groups. Or a table mapping bone groups to its priority (default: PRIORITY.Default). +-- * `blendMask` - A mask of which bone groups to include in the animation (Default: BLEND_MASK.All. +-- * `autodisable` - If true, the animation will be immediately removed upon finishing, which means information will not be possible to query once completed. (Default: true) +-- * `speed` - a floating point number >= 0, the speed at which the animation should play (default: 1) +-- * `startkey` - the animation key at which the animation should start (default: "start") +-- * `stopkey` - the animation key at which the animation should end (default: "stop") +-- * `startpoint` - a floating point number 0 <= value <= 1, starting completion of the animation (default: 0) +-- * `forceloop` - a boolean, to set if the animation should loop even if it's not a looping animation (default: false) + +--- +-- Check if the actor's animation has the given animation group or not. +-- @function [parent=#animation] hasGroup +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #boolean + +--- +-- Check if the actor's skeleton has the given bone or not +-- @function [parent=#animation] hasBone +-- @param openmw.core#GameObject actor +-- @param #string bonename +-- @return #boolean + +--- +-- Get the current active animation for a bone group +-- @function [parent=#animation] getActiveGroup +-- @param openmw.core#GameObject actor +-- @param #number bonegroup Bone group enum, see @{openmw.animation#BONE_GROUP} +-- @return #string + +--- +-- Plays a VFX on the actor. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] addVfx +-- @param openmw.core#GameObject actor +-- @param #any static @{openmw.core#StaticRecord} or #string ID +-- @param #table options optional table of parameters. Can contain: +-- +-- * `loop` - boolean, if true the effect will loop until removed (default: 0). +-- * `bonename` - name of the bone to attach the vfx to. (default: "") +-- * `particle` - name of the particle texture to use. (default: "") +-- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: ""). +-- +-- @usage local mgef = core.magic.effects[myEffectName] +-- anim.addVfx(self, 'VFX_Hands', {bonename = 'Bip01 L Hand', particle = mgef.particle, loop = mgef.continuousVfx, vfxId = mgef.id..'_myuniquenamehere'}) +-- -- later: +-- anim.removeVfx(self, mgef.id..'_myuniquenamehere') +-- + +--- +-- Removes a specific VFX +-- Can be used only in local scripts on self. +-- @function [parent=#animation] removeVfx +-- @param openmw.core#GameObject actor +-- @param #number vfxId an integer ID that uniquely identifies the VFX to remove + +--- +-- Removes all vfx from the actor +-- Can be used only in local scripts on self. +-- @function [parent=#animation] removeAllVfx +-- @param openmw.core#GameObject actor + + + + +return nil + diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 08382870f0..db60346e7c 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -297,6 +297,7 @@ -- @field #number gridY Index of the cell by Y (only for exteriors). -- @field #string worldSpaceId Id of the world space. -- @field #boolean hasWater True if the cell contains water. +-- @field #number waterLevel The water level of the cell. (nil if cell has no water). -- @field #boolean hasSky True if in this cell sky should be rendered. --- @@ -667,6 +668,11 @@ -- @field #number baseCost -- @field openmw.util#Color color -- @field #boolean harmful +-- @field #boolean continuousVfx Whether the magic effect's vfx should loop or not +-- @field #string particle Identifier of the particle texture +-- @field #string castingStatic Identifier of the vfx static used for casting +-- @field #string hitStatic Identifier of the vfx static used on hit +-- @field #string areaStatic Identifier of the vfx static used for AOE spells --- -- @type MagicEffectWithParams @@ -898,4 +904,24 @@ -- @field #number favouredSkillValue Secondary skill value required to get this rank. -- @field #number factionReaction Reaction of faction members if player is in this faction. +--- @{#VFX}: Visual effects +-- @field [parent=#core] #VFX vfx + +--- +-- Spawn a VFX at the given location in the world +-- @function [parent=#VFX] spawn +-- @param #any static openmw.core#StaticRecord or #string ID +-- @param openmw.util#Vector3 location +-- @param #table options optional table of parameters. Can contain: +-- +-- * `mwMagicVfx` - Boolean that if true causes the textureOverride parameter to only affect nodes with the Nif::RC_NiTexturingProperty property set. (default: true). +-- * `particleTextureOverride` - Name of a particle texture that should override this effect's default texture. (default: "") +-- * `scale` - A number that scales the size of the vfx (Default: 1) +-- +-- @usage -- Spawn a sanctuary effect near the player +-- local effect = core.magic.effects[core.magic.EFFECT_TYPE.Sanctuary] +-- pos = self.position + util.vector3(0, 100, 0) +-- core.vfx.spawn(effect.castingStatic, pos) +-- + return nil diff --git a/files/lua_api/openmw/interfaces.lua b/files/lua_api/openmw/interfaces.lua index d4a290aa47..57103768d2 100644 --- a/files/lua_api/openmw/interfaces.lua +++ b/files/lua_api/openmw/interfaces.lua @@ -5,6 +5,9 @@ --- -- @field [parent=#interfaces] scripts.omw.activationhandlers#scripts.omw.activationhandlers Activation +--- +-- @field [parent=#interfaces] scripts.omw.mechanics.animationcontroller#scripts.omw.mechanics.animationcontroller AnimationController + --- -- @field [parent=#interfaces] scripts.omw.ai#scripts.omw.ai AI diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 64b5c74866..dd13c8b565 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -717,6 +717,11 @@ -- @param #any objectOrRecordId -- @return #CreatureRecord +--- +-- @type CreatureAttack +-- @field #number minDamage Minimum attack damage. +-- @field #number maxDamage Maximum attack damage. + --- -- @type CreatureRecord -- @field #string id The record ID of the creature @@ -727,6 +732,10 @@ -- @field #number soulValue The soul value of the creature record -- @field #number type The @{#Creature.TYPE} of the creature -- @field #number baseGold The base barter gold of the creature +-- @field #number combatSkill The base combat skill of the creature. This is the skill value used for all skills with a 'combat' specialization +-- @field #number magicSkill The base magic skill of the creature. This is the skill value used for all skills with a 'magic' specialization +-- @field #number stealthSkill The base stealth skill of the creature. This is the skill value used for all skills with a 'stealth' specialization +-- @field #list<#number> attack A table of the 3 randomly selected attacks used by creatures that do not carry weapons. The table consists of 6 numbers split into groups of 2 values corresponding to minimum and maximum damage in that order. -- @field #map<#string, #boolean> servicesOffered The services of the creature, in a table. Value is if the service is provided or not, and they are indexed by: Spells, Spellmaking, Enchanting, Training, Repair, Barter, Weapon, Armor, Clothing, Books, Ingredients, Picks, Probes, Lights, Apparatus, RepairItems, Misc, Potions, MagicItems, Travel.