From 01dcca3363d2328b64c04cf7f4571790f7aff036 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 1 Jan 2024 17:21:00 +0300 Subject: [PATCH 01/59] Make scripted animations shut down pathfinding (bug #5065) --- CHANGELOG.md | 1 + apps/openmw/mwbase/mechanicsmanager.hpp | 2 ++ apps/openmw/mwmechanics/actors.cpp | 8 ++++++++ apps/openmw/mwmechanics/actors.hpp | 1 + apps/openmw/mwmechanics/aipackage.cpp | 11 ++++++----- apps/openmw/mwmechanics/character.hpp | 2 +- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 8 ++++++++ apps/openmw/mwmechanics/mechanicsmanagerimp.hpp | 1 + 8 files changed, 28 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dbd4e463f..0d9e199f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Bug #4822: Non-weapon equipment and body parts can't inherit time from parent animation 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 diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index b8e0fd1bde..9e99a37ec7 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -187,6 +187,8 @@ 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; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 25c4c97504..bc3cc3bb6a 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -2034,6 +2034,14 @@ 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) diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 15a39136a6..7c676ca018 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -116,6 +116,7 @@ namespace MWMechanics const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false) const; 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 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.hpp b/apps/openmw/mwmechanics/character.hpp index ee26b61a25..dc551900b5 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -217,7 +217,6 @@ namespace MWMechanics std::string chooseRandomAttackAnimation() const; static bool isRandomAttackAnimation(std::string_view group); - bool isScriptedAnimPlaying() const; bool isMovementAnimationControlled() const; void updateAnimQueue(); @@ -278,6 +277,7 @@ namespace MWMechanics bool playGroup(std::string_view groupname, int mode, int count, bool scripted = false); void skipAnim(); bool isAnimPlaying(std::string_view groupName) const; + bool isScriptedAnimPlaying() const; enum KillResult { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 59b1392dc9..c9e8e8f322 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -771,6 +771,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()) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 997636522e..f2483396fe 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -145,6 +145,7 @@ namespace MWMechanics const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false) 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; /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently From 1f26485c478e901978124166fb1e1e796c58ceeb Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sat, 6 Jan 2024 05:18:30 +0300 Subject: [PATCH 02/59] Fix exterior sun direction/position (bug #4898) --- CHANGELOG.md | 1 + apps/openmw/mwrender/renderingmanager.cpp | 3 +++ apps/openmw/mwworld/weather.cpp | 16 ++++++++-------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dbd4e463f..1771d0ab81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ 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 #5066: Quirks with starting and stopping scripted animations diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index fa92fa1420..2e3698a197 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -716,6 +716,9 @@ 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); diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index aa75730b40..584f73520c 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -752,21 +752,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); } From 9b8d6855784da859e77dc0ebc8d7c2c361f2e761 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sat, 6 Jan 2024 21:58:12 +0300 Subject: [PATCH 03/59] Expose requested apparent sun position (not normalized) to post-processing --- apps/openmw/mwrender/renderingmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 2e3698a197..c2c6abd1bc 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -721,7 +721,7 @@ namespace MWRender 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) From 3af28439d6dd4453825f69ea7c95c127d4249461 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sat, 6 Jan 2024 19:41:18 +0300 Subject: [PATCH 04/59] Interrupt thunder SFX indoors (bug #6402) --- CHANGELOG.md | 1 + apps/openmw/mwworld/weather.cpp | 15 ++++++++++++--- apps/openmw/mwworld/weather.hpp | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 181116ca9d..aea5b6b0a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,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 diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 2c9b80bc5f..655cd5aa7a 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) { @@ -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; From 69cf507db85845446b394129dc8955f3803dfde0 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 8 Jan 2024 22:17:02 +0100 Subject: [PATCH 05/59] Fix navmesh update on player changing tile In cases when objects are not present on the scene (e.g. generated exterior cells) navmesh is not updated because area that suppose to be covered with it was not updated. It was updated only during cell change. This is a regression from d15e1dca84. Set TileCachedRecastMeshManager range on NavMeshManager update to make sure it always covers correct area around player. Return a union of objects, heightfields and water ranges from getLimitedObjectsRange intersected with range provided above. --- .../detournavigator/gettilespositions.cpp | 76 +++++++ .../detournavigator/navigator.cpp | 185 +++++++++++++++--- .../detournavigator/operators.hpp | 18 ++ .../detournavigator/gettilespositions.cpp | 9 + .../detournavigator/gettilespositions.hpp | 2 + components/detournavigator/navmeshmanager.cpp | 1 + .../tilecachedrecastmeshmanager.cpp | 39 +++- 7 files changed, 299 insertions(+), 31 deletions(-) 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/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) From e67d6c6ebfcf4688f054604ec0d8add4b9ae2490 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 10 Jan 2024 12:30:19 +0400 Subject: [PATCH 06/59] Refactor Lua properties --- apps/openmw/mwlua/inputbindings.cpp | 33 ++++++++++++++++++----------- apps/openmw/mwlua/stats.cpp | 5 +++-- apps/openmw/mwlua/uibindings.cpp | 4 ++-- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index 33b19f3b4d..b44a528f2c 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -76,13 +76,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] @@ -102,11 +107,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/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/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 843917275c..30f190ad38 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -161,8 +161,8 @@ namespace MWLua api["_getMenuTransparency"] = []() -> float { return Settings::gui().mMenuTransparency; }; auto uiLayer = context.mLua->sol().new_usertype("UiLayer"); - uiLayer["name"] = sol::property([](LuaUi::Layer& self) { return self.name(); }); - uiLayer["size"] = sol::property([](LuaUi::Layer& self) { return self.size(); }); + uiLayer["name"] = sol::readonly_property([](LuaUi::Layer& self) -> std::string_view { return self.name(); }); + uiLayer["size"] = sol::readonly_property([](LuaUi::Layer& self) { return self.size(); }); uiLayer[sol::meta_function::to_string] = [](LuaUi::Layer& self) { return Misc::StringUtils::format("UiLayer(%s)", self.name()); }; From 0e1bb4534557425cab49cc1721293a07d25a3d8f Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 10 Jan 2024 14:24:18 +0400 Subject: [PATCH 07/59] Cleanup navmesh updater --- components/detournavigator/asyncnavmeshupdater.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 3a1b6fd77a..980281240d 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); From ccbc02abc3da4f1a124df9de5b4b1022cda48f92 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Wed, 10 Jan 2024 08:07:48 +0300 Subject: [PATCH 08/59] Handle running stats extensions on non-actors gracefully (#7770) --- CHANGELOG.md | 1 + apps/openmw/mwscript/statsextensions.cpp | 143 +++++++++++++++++++---- 2 files changed, 120 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f07def9ac..32d0a6a866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,7 @@ 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 #7770: Sword of the Perithia: Script execution failure Feature #2566: Handle NAM9 records for manual cell references Feature #3537: Shader-based water ripples Feature #5173: Support for NiFogProperty 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); } }; From c4ed812567b1ac7b8b25bd962794e8ce000cbcd2 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 11 Jan 2024 03:12:13 +0300 Subject: [PATCH 09/59] Properly redraw the topics list when disposition bar state changes --- apps/openmw/mwgui/dialogue.cpp | 32 ++++++++++++++------------------ apps/openmw/mwgui/dialogue.hpp | 1 + 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 0e44b8c03e..ce79e2834c 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -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; From 52623ddd7d153520f956b6cc48c4d66e97e016f0 Mon Sep 17 00:00:00 2001 From: Yury Stepovikov Date: Thu, 11 Jan 2024 00:59:27 +0000 Subject: [PATCH 10/59] Set MacOS current_path before reading configuration files [#7706] --- AUTHORS.md | 1 + apps/launcher/main.cpp | 5 ----- apps/opencs/main.cpp | 5 ----- apps/openmw/main.cpp | 2 -- apps/wizard/main.cpp | 2 -- components/files/macospath.cpp | 33 +++++++++++++++++++++++++++++++++ 6 files changed, 34 insertions(+), 14 deletions(-) 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/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/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/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/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/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 From 1880894f4ad943b05561226b7f4ec6889efdb299 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 11 Jan 2024 19:05:37 +0100 Subject: [PATCH 11/59] Use ciEqual to detect missing content files --- apps/openmw/mwstate/statemanagerimp.cpp | 5 ++--- components/esm3/savedgame.cpp | 6 +++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 4358c4094e..631ef9a112 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; 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); } From 3ad79e3b3eb7d254128cc43696ca451940201ff6 Mon Sep 17 00:00:00 2001 From: elsid Date: Tue, 26 Dec 2023 12:16:14 +0100 Subject: [PATCH 12/59] Pregenerate glow texture names To avoid strings generation and allocations every time model is added to a scene. --- components/sceneutil/util.cpp | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index 6ff366f76e..b71de4c2a3 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -1,6 +1,7 @@ #include "util.hpp" #include +#include #include #include @@ -17,6 +18,26 @@ namespace SceneUtil { + namespace + { + 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; + } + + const std::array glowTextureNames = generateGlowTextureNames(); + } class FindLowestUnusedTexUnitVisitor : public osg::NodeVisitor { @@ -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); From 35da9f8c50325331a674b45445809b68be994933 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 12 Jan 2024 01:49:17 +0100 Subject: [PATCH 13/59] Remove redundant SizeProxy and RenderTarget constructors --- components/fx/types.hpp | 20 -------------------- 1 file changed, 20 deletions(-) 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 From 1bfcfaff341f65666fb9d9f774f9baeae1e425ce Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 12 Jan 2024 03:35:57 +0100 Subject: [PATCH 14/59] Use proper naming for member variable --- components/esm3/aisequence.cpp | 2 +- components/esm3/aisequence.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/esm3/aisequence.cpp b/components/esm3/aisequence.cpp index 99c85db1bb..d5b15893bf 100644 --- a/components/esm3/aisequence.cpp +++ b/components/esm3/aisequence.cpp @@ -14,7 +14,7 @@ namespace ESM 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.getHNT("STAR", mDurationData.mRemainingDuration, mDurationData.mUnused); // was mStartTime mStoredInitialActorPosition = esm.getHNOT("POS_", mInitialActorPosition.mValues); } diff --git a/components/esm3/aisequence.hpp b/components/esm3/aisequence.hpp index 107fdf3bdb..099e5560e1 100644 --- a/components/esm3/aisequence.hpp +++ b/components/esm3/aisequence.hpp @@ -48,7 +48,7 @@ namespace ESM struct AiWanderDuration { float mRemainingDuration; - int32_t unused; + int32_t mUnused; }; struct AiTravelData { From 074ab682abc31b7f844da6cad79818a60ce016e5 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 12 Jan 2024 10:04:56 +0400 Subject: [PATCH 15/59] Move local variables in the editor --- apps/opencs/model/world/data.cpp | 4 ++-- apps/opencs/model/world/idcompletionmanager.cpp | 2 +- apps/opencs/view/doc/adjusterwidget.cpp | 2 +- apps/opencs/view/doc/viewmanager.cpp | 2 +- apps/opencs/view/render/pagedworldspacewidget.cpp | 2 +- apps/opencs/view/render/terraintexturemode.cpp | 4 ++-- apps/opencs/view/widget/scenetooltexturebrush.cpp | 2 +- apps/opencs/view/world/extendedcommandconfigurator.cpp | 2 +- apps/opencs/view/world/tablesubview.cpp | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 6322a77e66..601ee8d263 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -1107,7 +1107,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 +1123,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/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/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..1d4dc37529 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); } From 3592dc4c8835d49a076007296714019a9ec83230 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 12 Jan 2024 03:01:49 +0100 Subject: [PATCH 16/59] Add tests for saving and loading AiSequence::AiWander --- apps/openmw_test_suite/esm3/testsaveload.cpp | 82 ++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/apps/openmw_test_suite/esm3/testsaveload.cpp b/apps/openmw_test_suite/esm3/testsaveload.cpp index ff68d0d4f1..501a0b47c0 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,87 @@ 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.mDurationData.mUnused = 14; + 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.mDurationData.mUnused, record.mDurationData.mUnused); + 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())); } } From dd706aab0e118fdd185615558e6fc1b9fdf6e9e2 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 12 Jan 2024 23:01:09 +0100 Subject: [PATCH 17/59] Add missing SubPass::mMinMap initialization --- components/fx/technique.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)) From 384a1dd13acbbaaa91f35bafed473b9cbb6799f3 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 13 Jan 2024 00:44:07 +0100 Subject: [PATCH 18/59] Update PrecipitationOccluder only when there is precipitation --- apps/openmw/mwrender/precipitationocclusion.cpp | 17 +++++++++++++---- apps/openmw/mwrender/precipitationocclusion.hpp | 4 +++- 2 files changed, 16 insertions(+), 5 deletions(-) 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; }; } From 14e6af8bea50938bb610624166d1c90626af08b8 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sat, 13 Jan 2024 15:42:17 +0400 Subject: [PATCH 19/59] Add a table with fadeOut argument for streamMusic --- apps/openmw/mwlua/soundbindings.cpp | 21 +++++++++++++++++++-- files/lua_api/openmw/ambient.lua | 12 +++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwlua/soundbindings.cpp b/apps/openmw/mwlua/soundbindings.cpp index dc45a672b4..c3c810e591 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 @@ -95,9 +111,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/files/lua_api/openmw/ambient.lua b/files/lua_api/openmw/ambient.lua index 917ec86c85..7153a6fb44 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 From f8c1d48c0befb1b5a9bc089ad2454406caa723ed Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 14 Jan 2024 10:54:51 +0400 Subject: [PATCH 20/59] Get rid of redundant casts --- apps/opencs/view/render/worldspacewidget.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index da02c1e179..f7732d752d 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()); } @@ -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); } From 98b281e4ad1145e07a6fb984ac9b290c6c95910c Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 14 Jan 2024 10:59:39 +0400 Subject: [PATCH 21/59] Add a missing assertion --- apps/openmw/mwrender/pingpongcanvas.cpp | 2 ++ 1 file changed, 2 insertions(+) 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(); From 1a629cbf076a077d6d6ae9266fa9603a729ac2be Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 14 Jan 2024 10:09:46 +0300 Subject: [PATCH 22/59] Play shield hit sound for the shield that was hit (#7774) --- apps/openmw/mwclass/actor.cpp | 17 ----------------- apps/openmw/mwclass/actor.hpp | 2 -- apps/openmw/mwclass/creature.cpp | 3 --- apps/openmw/mwclass/npc.cpp | 3 --- apps/openmw/mwmechanics/combat.cpp | 9 +++++++++ apps/openmw/mwworld/class.cpp | 5 ----- apps/openmw/mwworld/class.hpp | 4 ---- 7 files changed, 9 insertions(+), 34 deletions(-) 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..ab68cddda7 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -339,10 +339,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); diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 4f295f7b35..f669547d1a 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -678,10 +678,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/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/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index d5062d6add..88d9d744e2 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 { diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 87e70b3198..55cc62c78d 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). From 6cefe2c118c8de160d61008a40d02bfc09b43901 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sat, 13 Jan 2024 21:34:44 +0400 Subject: [PATCH 23/59] Rework launcher tabs --- apps/launcher/graphicspage.cpp | 106 -- apps/launcher/graphicspage.hpp | 1 - apps/launcher/settingspage.cpp | 101 ++ apps/launcher/settingspage.hpp | 1 + apps/launcher/ui/graphicspage.ui | 632 +++---- apps/launcher/ui/importpage.ui | 22 +- apps/launcher/ui/mainwindow.ui | 18 +- apps/launcher/ui/settingspage.ui | 1482 ++++++++++------- .../source/reference/modding/settings/GUI.rst | 6 +- .../source/reference/modding/settings/fog.rst | 8 + .../reference/modding/settings/game.rst | 44 +- .../reference/modding/settings/general.rst | 4 +- .../reference/modding/settings/shadows.rst | 20 + .../reference/modding/settings/sound.rst | 9 +- .../reference/modding/settings/video.rst | 14 +- 15 files changed, 1263 insertions(+), 1205 deletions(-) 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/settingspage.cpp b/apps/launcher/settingspage.cpp index b8539671b5..9492326cb0 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -212,6 +212,55 @@ bool Launcher::SettingsPage::loadSettings() loadSettingBool(Settings::fog().mExponentialFog, *exponentialFogCheckBox); loadSettingBool(Settings::fog().mSkyBlending, *skyBlendingCheckBox); skyBlendingStartComboBox->setValue(Settings::fog().mSkyBlendingStart); + + 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); + + 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); } // Audio @@ -359,6 +408,52 @@ 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"); } // Audio @@ -461,3 +556,9 @@ void Launcher::SettingsPage::slotSkyBlendingToggled(bool checked) skyBlendingStartComboBox->setEnabled(checked); skyBlendingStartLabel->setEnabled(checked); } + +void Launcher::SettingsPage::slotShadowDistLimitToggled(bool checked) +{ + shadowDistanceSpinBox->setEnabled(checked); + fadeStartSpinBox->setEnabled(checked); +} diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index 9f7d6b1f43..a8a6b7c26d 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -32,6 +32,7 @@ namespace Launcher void slotAnimSourcesToggled(bool checked); void slotPostProcessToggled(bool checked); void slotSkyBlendingToggled(bool checked); + void slotShadowDistLimitToggled(bool checked); 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..cf8215ef30 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,746 @@ - - - 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 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + legacy + + + + + shaders compatibility + + + + + shaders + + + + + + + + Lighting method + + + + + + + + + + @@ -786,7 +1080,7 @@ Select your preferred audio device. - Audio Device + Audio device @@ -872,7 +1166,7 @@ Select your preferred HRTF profile. - HRTF Profile + HRTF profile @@ -936,17 +1230,17 @@ - Tool Tip Only + Tooltip - Crosshair Only + Crosshair - Tool Tip and Crosshair + Tooltip and crosshair @@ -1036,16 +1330,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 +1394,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 +1420,7 @@ - Maximum Quicksaves + Maximum quicksaves @@ -1193,9 +1437,9 @@ - + - Other + Screenshots @@ -1203,7 +1447,7 @@ - Screenshot Format + Screenshot format 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 --------------- From 6ff14e19d18fe19ff474e2de1f21d05391bde51d Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 14 Jan 2024 16:41:55 +0100 Subject: [PATCH 24/59] Make cell models preloading a const operation --- apps/openmw/mwclass/creature.cpp | 11 ++-- apps/openmw/mwclass/creature.hpp | 4 +- apps/openmw/mwclass/creaturelevlist.cpp | 19 ------ apps/openmw/mwclass/creaturelevlist.hpp | 4 -- apps/openmw/mwclass/npc.cpp | 79 +++++++++++++------------ apps/openmw/mwclass/npc.hpp | 4 +- apps/openmw/mwworld/cellpreloader.cpp | 13 +--- apps/openmw/mwworld/cellstore.hpp | 12 ++-- apps/openmw/mwworld/class.cpp | 4 +- apps/openmw/mwworld/class.hpp | 4 +- 10 files changed, 63 insertions(+), 91 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index bb9c1bc277..fbe232093a 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); @@ -513,7 +514,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/npc.cpp b/apps/openmw/mwclass/npc.cpp index 4f295f7b35..7ff26fbdb6 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)); } 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/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..a8f296045a 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -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; @@ -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; diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index d5062d6add..c8ba337cee 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -149,7 +149,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 +320,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..694a40cb32 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -170,7 +170,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 +284,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(). From a91e557c68845fdfa66801a4976f1c4c91bed040 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 14 Jan 2024 22:10:18 +0400 Subject: [PATCH 25/59] Fix Touch command (bug 7765) --- CHANGELOG.md | 1 + apps/opencs/model/world/commands.cpp | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e6f05dd2..e0dbe3960a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,7 @@ 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 Feature #2566: Handle NAM9 records for manual cell references Feature #3537: Shader-based water ripples 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() From 645175089016127a252e2172ef5855d0dda499d5 Mon Sep 17 00:00:00 2001 From: elsid Date: Fri, 12 Jan 2024 02:15:54 +0100 Subject: [PATCH 26/59] Write AiSequence and Script data field by field via decompose function Use the same function to load and save to have single place with field order definition. Use concepts for overload over different types. --- apps/openmw_test_suite/esm3/testsaveload.cpp | 2 - components/esm/decompose.hpp | 10 ++++ components/esm3/aisequence.cpp | 50 +++++++++++++++----- components/esm3/aisequence.hpp | 13 +++-- components/esm3/esmreader.hpp | 12 +++++ components/esm3/esmwriter.hpp | 17 ++++++- components/esm3/loadscpt.cpp | 15 +++--- components/misc/concepts.hpp | 13 +++++ 8 files changed, 104 insertions(+), 28 deletions(-) create mode 100644 components/esm/decompose.hpp create mode 100644 components/misc/concepts.hpp diff --git a/apps/openmw_test_suite/esm3/testsaveload.cpp b/apps/openmw_test_suite/esm3/testsaveload.cpp index 501a0b47c0..f8ef23e887 100644 --- a/apps/openmw_test_suite/esm3/testsaveload.cpp +++ b/apps/openmw_test_suite/esm3/testsaveload.cpp @@ -422,7 +422,6 @@ namespace ESM std::copy(std::begin(idle), std::end(idle), record.mData.mIdle); record.mData.mShouldRepeat = 12; record.mDurationData.mRemainingDuration = 13; - record.mDurationData.mUnused = 14; record.mStoredInitialActorPosition = true; constexpr float initialActorPosition[3] = { 15, 16, 17 }; static_assert(std::size(initialActorPosition) == std::size(record.mInitialActorPosition.mValues)); @@ -438,7 +437,6 @@ namespace ESM 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.mDurationData.mUnused, record.mDurationData.mUnused); EXPECT_EQ(result.mStoredInitialActorPosition, record.mStoredInitialActorPosition); EXPECT_THAT(result.mInitialActorPosition.mValues, ElementsAreArray(record.mInitialActorPosition.mValues)); } 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 d5b15893bf..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.mUnused); // 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 099e5560e1..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 mUnused; }; + 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/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/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 From 4d6350539c860aa9c0711d79b338f4d87ad44f2d Mon Sep 17 00:00:00 2001 From: elsid Date: Tue, 26 Dec 2023 12:36:23 +0100 Subject: [PATCH 27/59] Move FindLowestUnusedTexUnitVisitor to unnamed namespace It's not used anywhere except this translation unit so no need to make the symbol available everywhere else. --- components/sceneutil/util.cpp | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index b71de4c2a3..ce48702a74 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -37,27 +37,27 @@ namespace SceneUtil } const std::array glowTextureNames = generateGlowTextureNames(); + + 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; + }; } - class FindLowestUnusedTexUnitVisitor : public osg::NodeVisitor - { - public: - FindLowestUnusedTexUnitVisitor() - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mLowestUnusedTexUnit(0) - { - } - - void apply(osg::Node& node) override - { - if (osg::StateSet* stateset = node.getStateSet()) - mLowestUnusedTexUnit = std::max(mLowestUnusedTexUnit, int(stateset->getTextureAttributeList().size())); - - traverse(node); - } - int mLowestUnusedTexUnit; - }; - GlowUpdater::GlowUpdater(int texUnit, const osg::Vec4f& color, const std::vector>& textures, osg::Node* node, float duration, Resource::ResourceSystem* resourcesystem) From a2147d70cc45d60a4f2d048980a5bcd5496d4b55 Mon Sep 17 00:00:00 2001 From: elsid Date: Tue, 16 Jan 2024 00:30:41 +0100 Subject: [PATCH 28/59] Use forward declaration for some VFS types This will allow to save on preprocessed code size in the future changes. --- apps/niftest/niftest.cpp | 1 + apps/opencs/model/world/resources.cpp | 1 + apps/openmw/mwgui/loadingscreen.cpp | 1 + apps/openmw/mwgui/settingswindow.cpp | 1 + apps/openmw/mwlua/vfsbindings.cpp | 1 + apps/openmw/mwrender/animation.cpp | 1 + apps/openmw/mwrender/postprocessor.cpp | 1 + apps/openmw/mwsound/soundmanagerimp.cpp | 1 + apps/openmw_test_suite/testing_util.hpp | 1 + components/vfs/archive.hpp | 22 ++------ components/vfs/bsaarchive.hpp | 1 + components/vfs/file.hpp | 21 ++++++++ components/vfs/filemap.hpp | 14 +++++ components/vfs/filesystemarchive.hpp | 3 +- components/vfs/manager.cpp | 9 +++- components/vfs/manager.hpp | 49 +++-------------- components/vfs/recursivedirectoryiterator.hpp | 53 +++++++++++++++++++ 17 files changed, 120 insertions(+), 61 deletions(-) create mode 100644 components/vfs/file.hpp create mode 100644 components/vfs/filemap.hpp create mode 100644 components/vfs/recursivedirectoryiterator.hpp diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index 29488fb677..32fd65c348 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include 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/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/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index fbd54586df..0ffadc43a3 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" 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/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 7fa8e43c37..feed9719b6 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index 1aaeb460b7..c31f49f35a 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" diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 64f8959218..383d316c91 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" diff --git a/apps/openmw_test_suite/testing_util.hpp b/apps/openmw_test_suite/testing_util.hpp index 0c941053a7..aa76f7f944 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 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..808153fc05 --- /dev/null +++ b/components/vfs/filemap.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_COMPONENTS_VFS_FILEMAP_H +#define OPENMW_COMPONENTS_VFS_FILEMAP_H + +#include +#include + +namespace VFS +{ + class File; + + 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..5315f17252 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,7 +77,7 @@ 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() }; 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/recursivedirectoryiterator.hpp b/components/vfs/recursivedirectoryiterator.hpp new file mode 100644 index 0000000000..82f8e594fd --- /dev/null +++ b/components/vfs/recursivedirectoryiterator.hpp @@ -0,0 +1,53 @@ +#ifndef OPENMW_COMPONENTS_VFS_RECURSIVEDIRECTORYITERATOR_H +#define OPENMW_COMPONENTS_VFS_RECURSIVEDIRECTORYITERATOR_H + +#include + +#include "filemap.hpp" + +namespace VFS +{ + 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; } + + 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 From a340b49cbc51455ff38910f3a941a3ca2d5f4fb6 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 16 Jan 2024 10:23:13 +0400 Subject: [PATCH 29/59] Enhance light settings tweaking --- apps/launcher/settingspage.cpp | 54 +++++-- apps/launcher/settingspage.hpp | 1 + apps/launcher/ui/settingspage.ui | 138 +++++++++++++++++- apps/openmw/mwgui/settingswindow.cpp | 2 +- files/data/l10n/OMWEngine/de.yaml | 4 + files/data/l10n/OMWEngine/en.yaml | 4 + files/data/l10n/OMWEngine/fr.yaml | 4 + files/data/l10n/OMWEngine/ru.yaml | 4 + files/data/l10n/OMWEngine/sv.yaml | 4 + .../data/mygui/openmw_settings_window.layout | 9 +- 10 files changed, 200 insertions(+), 24 deletions(-) diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 9492326cb0..c274b75f79 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -213,21 +213,6 @@ bool Launcher::SettingsPage::loadSettings() loadSettingBool(Settings::fog().mSkyBlending, *skyBlendingCheckBox); skyBlendingStartComboBox->setValue(Settings::fog().mSkyBlendingStart); - 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); - loadSettingBool(Settings::shadows().mActorShadows, *actorShadowsCheckBox); loadSettingBool(Settings::shadows().mPlayerShadows, *playerShadowsCheckBox); loadSettingBool(Settings::shadows().mTerrainShadows, *terrainShadowsCheckBox); @@ -261,6 +246,31 @@ bool Launcher::SettingsPage::loadSettings() 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 @@ -454,6 +464,12 @@ void Launcher::SettingsPage::saveSettings() 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 @@ -562,3 +578,11 @@ 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 a8a6b7c26d..ea675857ea 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -33,6 +33,7 @@ namespace Launcher 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/settingspage.ui b/apps/launcher/ui/settingspage.ui index cf8215ef30..7006238e71 100644 --- a/apps/launcher/ui/settingspage.ui +++ b/apps/launcher/ui/settingspage.ui @@ -1018,7 +1018,17 @@ - + + + + <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 @@ -1031,29 +1041,143 @@ + + + + <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 + Legacy - shaders compatibility + Shaders (compatibility) - shaders + 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> + - Lighting method + 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 diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index fbd54586df..6a1cfaefe2 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -131,7 +131,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/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 @@ - + From 27fa411f4f701985079da35f91c22c54a3626fe8 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 16 Jan 2024 20:56:58 +0100 Subject: [PATCH 30/59] Convert strings in nif files to utf8 --- apps/bulletobjecttool/main.cpp | 2 +- apps/navmeshtool/main.cpp | 2 +- apps/niftest/niftest.cpp | 2 +- apps/opencs/model/world/data.cpp | 2 +- apps/openmw/engine.cpp | 3 ++- components/nif/niffile.cpp | 5 +++-- components/nif/niffile.hpp | 8 +++++++- components/nif/nifstream.cpp | 4 ++++ components/nif/nifstream.hpp | 11 ++++++++++- components/resource/keyframemanager.cpp | 6 ++++-- components/resource/keyframemanager.hpp | 9 ++++++++- components/resource/niffilemanager.cpp | 9 +++++---- components/resource/niffilemanager.hpp | 9 ++++++++- components/resource/resourcesystem.cpp | 6 +++--- components/resource/resourcesystem.hpp | 7 ++++++- 15 files changed, 64 insertions(+), 21 deletions(-) diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp index 7d87899f4a..2165f93804 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); Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, expiryDelay); Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index 8604bcdfb0..a9c30cf23e 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); 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..5d0f723ee5 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -84,7 +84,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 diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 6322a77e66..ef2e289ee2 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -149,7 +149,7 @@ 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); Shader::ShaderManager::DefineMap defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 92483bd8c3..c2e57208b4 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -706,7 +706,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()); mResourceSystem->getSceneManager()->getShaderManager().setMaxTextureUnits(mGlMaxTextureImageUnits); mResourceSystem->getSceneManager()->setUnRefImageDataAfterApply( false); // keep to Off for now to allow better state sharing diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index d6d063a254..7a38b0dc1a 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::Utf8Encoder* 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(); diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp index 6f0030af47..38b0712373 100644 --- a/components/nif/niffile.hpp +++ b/components/nif/niffile.hpp @@ -11,6 +11,11 @@ #include "record.hpp" +namespace ToUTF8 +{ + class Utf8Encoder; +} + namespace Nif { @@ -112,6 +117,7 @@ namespace Nif std::vector mStrings; bool& mUseSkinning; + const ToUTF8::Utf8Encoder* 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); + Reader(NIFFile& file, const ToUTF8::Utf8Encoder* encoder); /// Parse the file void parse(Files::IStreamPtr&& stream); diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index 2eba746ccf..93714e37f0 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->getStatelessEncoder().getUtf8(str, ToUTF8::BufferAllocationPolicy::UseGrowFactor, mBuffer); return str; } diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp index 95205c4fda..958aef7254 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 Utf8Encoder; +} + namespace Nif { @@ -67,11 +73,14 @@ namespace Nif { const Reader& mReader; Files::IStreamPtr mStream; + const ToUTF8::Utf8Encoder* mEncoder; + std::string mBuffer; public: - explicit NIFStream(const Reader& reader, Files::IStreamPtr&& stream) + explicit NIFStream(const Reader& reader, Files::IStreamPtr&& stream, const ToUTF8::Utf8Encoder* encoder) : mReader(reader) , mStream(std::move(stream)) + , mEncoder(encoder) { } diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index 574d761d09..6895a0238f 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -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::Utf8Encoder* 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()); } diff --git a/components/resource/keyframemanager.hpp b/components/resource/keyframemanager.hpp index 0c92553949..f684e22ee7 100644 --- a/components/resource/keyframemanager.hpp +++ b/components/resource/keyframemanager.hpp @@ -9,6 +9,11 @@ #include "resourcemanager.hpp" +namespace ToUTF8 +{ + class Utf8Encoder; +} + 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::Utf8Encoder* encoder); ~KeyframeManager() = default; /// Retrieve a read-only keyframe resource by name (case-insensitive). @@ -59,6 +65,7 @@ namespace Resource private: SceneManager* mSceneManager; + const ToUTF8::Utf8Encoder* mEncoder; }; } diff --git a/components/resource/niffilemanager.cpp b/components/resource/niffilemanager.cpp index 5e457cdfaa..fb57bc8c85 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::Utf8Encoder* 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..2225a9fd4c 100644 --- a/components/resource/niffilemanager.hpp +++ b/components/resource/niffilemanager.hpp @@ -5,6 +5,11 @@ #include "resourcemanager.hpp" +namespace ToUTF8 +{ + class Utf8Encoder; +} + namespace Resource { @@ -12,8 +17,10 @@ namespace Resource /// @note May be used from any thread. class NifFileManager : public ResourceManager { + const ToUTF8::Utf8Encoder* mEncoder; + public: - NifFileManager(const VFS::Manager* vfs); + NifFileManager(const VFS::Manager* vfs, const ToUTF8::Utf8Encoder* 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..7d704e7d1e 100644 --- a/components/resource/resourcesystem.cpp +++ b/components/resource/resourcesystem.cpp @@ -10,13 +10,13 @@ namespace Resource { - ResourceSystem::ResourceSystem(const VFS::Manager* vfs, double expiryDelay) + ResourceSystem::ResourceSystem(const VFS::Manager* vfs, double expiryDelay, const ToUTF8::Utf8Encoder* 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..554083852e 100644 --- a/components/resource/resourcesystem.hpp +++ b/components/resource/resourcesystem.hpp @@ -15,6 +15,11 @@ namespace osg class State; } +namespace ToUTF8 +{ + class Utf8Encoder; +} + namespace Resource { @@ -30,7 +35,7 @@ namespace Resource class ResourceSystem { public: - explicit ResourceSystem(const VFS::Manager* vfs, double expiryDelay); + explicit ResourceSystem(const VFS::Manager* vfs, double expiryDelay, const ToUTF8::Utf8Encoder* encoder); ~ResourceSystem(); SceneManager* getSceneManager(); From 48db113149b1219073a4a7a43393292b53b96339 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Wed, 17 Jan 2024 18:10:42 +0100 Subject: [PATCH 31/59] Address feedback --- CHANGELOG.md | 1 + apps/bulletobjecttool/main.cpp | 2 +- apps/navmeshtool/main.cpp | 2 +- apps/opencs/model/world/data.cpp | 3 ++- apps/openmw/engine.cpp | 4 ++-- components/nif/niffile.cpp | 2 +- components/nif/niffile.hpp | 6 +++--- components/nif/nifstream.cpp | 2 +- components/nif/nifstream.hpp | 7 ++++--- components/resource/keyframemanager.cpp | 4 ++-- components/resource/keyframemanager.hpp | 6 +++--- components/resource/niffilemanager.cpp | 2 +- components/resource/niffilemanager.hpp | 6 +++--- components/resource/resourcesystem.cpp | 3 ++- components/resource/resourcesystem.hpp | 5 +++-- components/to_utf8/to_utf8.hpp | 2 +- 16 files changed, 31 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e6f05dd2..a35f44fe2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -129,6 +129,7 @@ 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 #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 2165f93804..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, &encoder); + Resource::NifFileManager nifFileManager(&vfs, &encoder.getStatelessEncoder()); Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, expiryDelay); Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index a9c30cf23e..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, &encoder); + 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/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index ef2e289ee2..428ffb2737 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, &mEncoder); + mResourceSystem + = std::make_unique(mVFS.get(), expiryDelay, &mEncoder.getStatelessEncoder()); Shader::ShaderManager::DefineMap defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index c2e57208b4..5b8d725583 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -706,8 +706,8 @@ void OMW::Engine::prepareEngine() VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); - mResourceSystem - = std::make_unique(mVFS.get(), Settings::cells().mCacheExpiryDelay, mEncoder.get()); + 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 diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index 7a38b0dc1a..74c3b391a1 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -24,7 +24,7 @@ namespace Nif { - Reader::Reader(NIFFile& file, const ToUTF8::Utf8Encoder* encoder) + Reader::Reader(NIFFile& file, const ToUTF8::StatelessUtf8Encoder* encoder) : mVersion(file.mVersion) , mUserVersion(file.mUserVersion) , mBethVersion(file.mBethVersion) diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp index 38b0712373..993e9b7eea 100644 --- a/components/nif/niffile.hpp +++ b/components/nif/niffile.hpp @@ -13,7 +13,7 @@ namespace ToUTF8 { - class Utf8Encoder; + class StatelessUtf8Encoder; } namespace Nif @@ -117,7 +117,7 @@ namespace Nif std::vector mStrings; bool& mUseSkinning; - const ToUTF8::Utf8Encoder* mEncoder; + const ToUTF8::StatelessUtf8Encoder* mEncoder; static std::atomic_bool sLoadUnsupportedFiles; static std::atomic_bool sWriteNifDebugLog; @@ -128,7 +128,7 @@ namespace Nif public: /// Open a NIF stream. The name is used for error messages. - Reader(NIFFile& file, const ToUTF8::Utf8Encoder* encoder); + 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 93714e37f0..f960e8d972 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -61,7 +61,7 @@ namespace Nif if (end != std::string::npos) str.erase(end); if (mEncoder) - str = mEncoder->getStatelessEncoder().getUtf8(str, ToUTF8::BufferAllocationPolicy::UseGrowFactor, mBuffer); + str = mEncoder->getUtf8(str, ToUTF8::BufferAllocationPolicy::UseGrowFactor, mBuffer); return str; } diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp index 958aef7254..062f7c6512 100644 --- a/components/nif/nifstream.hpp +++ b/components/nif/nifstream.hpp @@ -25,7 +25,7 @@ namespace ToUTF8 { - class Utf8Encoder; + class StatelessUtf8Encoder; } namespace Nif @@ -73,11 +73,12 @@ namespace Nif { const Reader& mReader; Files::IStreamPtr mStream; - const ToUTF8::Utf8Encoder* mEncoder; + const ToUTF8::StatelessUtf8Encoder* mEncoder; std::string mBuffer; public: - explicit NIFStream(const Reader& reader, Files::IStreamPtr&& stream, const ToUTF8::Utf8Encoder* encoder) + explicit NIFStream( + const Reader& reader, Files::IStreamPtr&& stream, const ToUTF8::StatelessUtf8Encoder* encoder) : mReader(reader) , mStream(std::move(stream)) , mEncoder(encoder) diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index 6895a0238f..d60129cb86 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -207,8 +207,8 @@ namespace Resource namespace Resource { - KeyframeManager::KeyframeManager( - const VFS::Manager* vfs, SceneManager* sceneManager, double expiryDelay, const ToUTF8::Utf8Encoder* encoder) + KeyframeManager::KeyframeManager(const VFS::Manager* vfs, SceneManager* sceneManager, double expiryDelay, + const ToUTF8::StatelessUtf8Encoder* encoder) : ResourceManager(vfs, expiryDelay) , mSceneManager(sceneManager) , mEncoder(encoder) diff --git a/components/resource/keyframemanager.hpp b/components/resource/keyframemanager.hpp index f684e22ee7..ed8d4a04ab 100644 --- a/components/resource/keyframemanager.hpp +++ b/components/resource/keyframemanager.hpp @@ -11,7 +11,7 @@ namespace ToUTF8 { - class Utf8Encoder; + class StatelessUtf8Encoder; } namespace Resource @@ -54,7 +54,7 @@ namespace Resource { public: explicit KeyframeManager(const VFS::Manager* vfs, SceneManager* sceneManager, double expiryDelay, - const ToUTF8::Utf8Encoder* encoder); + const ToUTF8::StatelessUtf8Encoder* encoder); ~KeyframeManager() = default; /// Retrieve a read-only keyframe resource by name (case-insensitive). @@ -65,7 +65,7 @@ namespace Resource private: SceneManager* mSceneManager; - const ToUTF8::Utf8Encoder* mEncoder; + const ToUTF8::StatelessUtf8Encoder* mEncoder; }; } diff --git a/components/resource/niffilemanager.cpp b/components/resource/niffilemanager.cpp index fb57bc8c85..352d367f9b 100644 --- a/components/resource/niffilemanager.cpp +++ b/components/resource/niffilemanager.cpp @@ -31,7 +31,7 @@ namespace Resource Nif::NIFFilePtr mNifFile; }; - NifFileManager::NifFileManager(const VFS::Manager* vfs, const ToUTF8::Utf8Encoder* encoder) + 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) diff --git a/components/resource/niffilemanager.hpp b/components/resource/niffilemanager.hpp index 2225a9fd4c..dab4b70748 100644 --- a/components/resource/niffilemanager.hpp +++ b/components/resource/niffilemanager.hpp @@ -7,7 +7,7 @@ namespace ToUTF8 { - class Utf8Encoder; + class StatelessUtf8Encoder; } namespace Resource @@ -17,10 +17,10 @@ namespace Resource /// @note May be used from any thread. class NifFileManager : public ResourceManager { - const ToUTF8::Utf8Encoder* mEncoder; + const ToUTF8::StatelessUtf8Encoder* mEncoder; public: - NifFileManager(const VFS::Manager* vfs, const ToUTF8::Utf8Encoder* encoder); + 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 7d704e7d1e..65a83a60ab 100644 --- a/components/resource/resourcesystem.cpp +++ b/components/resource/resourcesystem.cpp @@ -10,7 +10,8 @@ namespace Resource { - ResourceSystem::ResourceSystem(const VFS::Manager* vfs, double expiryDelay, const ToUTF8::Utf8Encoder* encoder) + ResourceSystem::ResourceSystem( + const VFS::Manager* vfs, double expiryDelay, const ToUTF8::StatelessUtf8Encoder* encoder) : mVFS(vfs) { mNifFileManager = std::make_unique(vfs, encoder); diff --git a/components/resource/resourcesystem.hpp b/components/resource/resourcesystem.hpp index 554083852e..f7f09b9277 100644 --- a/components/resource/resourcesystem.hpp +++ b/components/resource/resourcesystem.hpp @@ -17,7 +17,7 @@ namespace osg namespace ToUTF8 { - class Utf8Encoder; + class StatelessUtf8Encoder; } namespace Resource @@ -35,7 +35,8 @@ namespace Resource class ResourceSystem { public: - explicit ResourceSystem(const VFS::Manager* vfs, double expiryDelay, const ToUTF8::Utf8Encoder* encoder); + explicit ResourceSystem( + const VFS::Manager* vfs, double expiryDelay, const ToUTF8::StatelessUtf8Encoder* encoder); ~ResourceSystem(); SceneManager* getSceneManager(); 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; From 35d9b18b4c009f6adf56421ee59dd7d8f34e69f9 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 15 Jan 2024 22:26:56 +0100 Subject: [PATCH 32/59] Add type for normalized VFS path and use for VFS::Manager file map key This will reduce the number of path normalizations while more places will use this type. In some cases it also will reduce number of temporary allocations for new strings. For now make conversion from and to std::string_view implicit to allow gradual migration to this type. --- apps/openmw_test_suite/testing_util.hpp | 2 +- components/vfs/filemap.hpp | 7 +- components/vfs/manager.cpp | 2 +- components/vfs/pathutil.hpp | 68 +++++++++++++++++++ components/vfs/recursivedirectoryiterator.hpp | 5 +- 5 files changed, 79 insertions(+), 5 deletions(-) diff --git a/apps/openmw_test_suite/testing_util.hpp b/apps/openmw_test_suite/testing_util.hpp index aa76f7f944..b819848a8f 100644 --- a/apps/openmw_test_suite/testing_util.hpp +++ b/apps/openmw_test_suite/testing_util.hpp @@ -61,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/components/vfs/filemap.hpp b/components/vfs/filemap.hpp index 808153fc05..1b7d390d88 100644 --- a/components/vfs/filemap.hpp +++ b/components/vfs/filemap.hpp @@ -8,7 +8,12 @@ namespace VFS { class File; - using FileMap = std::map>; + namespace Path + { + class Normalized; + } + + using FileMap = std::map>; } #endif diff --git a/components/vfs/manager.cpp b/components/vfs/manager.cpp index 5315f17252..d312ce9d84 100644 --- a/components/vfs/manager.cpp +++ b/components/vfs/manager.cpp @@ -83,7 +83,7 @@ namespace VFS 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/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 index 82f8e594fd..39fb26e873 100644 --- a/components/vfs/recursivedirectoryiterator.hpp +++ b/components/vfs/recursivedirectoryiterator.hpp @@ -4,6 +4,7 @@ #include #include "filemap.hpp" +#include "pathutil.hpp" namespace VFS { @@ -15,9 +16,9 @@ namespace VFS { } - const std::string& operator*() const { return mIt->first; } + const std::string& operator*() const { return mIt->first.value(); } - const std::string* operator->() const { return &mIt->first; } + const std::string* operator->() const { return &mIt->first.value(); } RecursiveDirectoryIterator& operator++() { From 3cdb9496c4db9b9bbd103ce06d1659e713d69606 Mon Sep 17 00:00:00 2001 From: Cody Glassman Date: Thu, 18 Jan 2024 07:15:35 -0800 Subject: [PATCH 33/59] dont clear empty FBOs, fix doc example --- apps/openmw/mwrender/postprocessor.cpp | 5 +++++ docs/source/reference/postprocessing/omwfx.rst | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index 1aaeb460b7..c82104ee4a 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -662,6 +662,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/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) { From fba405587731f799899d815a6381b7cb4f121869 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Thu, 18 Jan 2024 22:32:46 +0100 Subject: [PATCH 34/59] Move return comments to a new line --- apps/openmw/mwworld/cellstore.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index a8f296045a..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) { @@ -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 { @@ -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) { From ffa52dfe7c3f2a9fa9a05b14dc174c4ec4e387e1 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Fri, 19 Jan 2024 11:55:37 +0300 Subject: [PATCH 35/59] Don't use height cull callback when there's no terrain --- components/terrain/world.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From e997c44db6df01c62882fba5e166e6d9c5143bc5 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Fri, 19 Jan 2024 12:53:35 +0300 Subject: [PATCH 36/59] Restore unwrapped Bullet triangle shape shallow copying --- components/resource/bulletshape.cpp | 7 +++++++ 1 file changed, 7 insertions(+) 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); From b37aee21e3796a8d7f7633a80311e120fe87cf04 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 19 Jan 2024 15:16:46 +0400 Subject: [PATCH 37/59] Fix tooltips in the main menu --- apps/openmw/engine.cpp | 6 +----- apps/openmw/mwworld/worldimp.cpp | 4 ++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 92483bd8c3..a059e7477f 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -321,11 +321,7 @@ bool OMW::Engine::frame(float frametime) // update GUI by world data { ScopedProfile 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 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); From 87c9f395f11712470bcd6c134b79002d3d6031da Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 19 Jan 2024 16:01:48 +0400 Subject: [PATCH 38/59] Move local variables in components --- components/bsa/ba2dx10file.cpp | 2 +- components/config/gamesettings.cpp | 2 +- components/detournavigator/asyncnavmeshupdater.cpp | 2 +- components/esm3/inventorystate.cpp | 2 +- components/esm3/spellstate.cpp | 4 ++-- components/esm4/loadfurn.cpp | 2 +- components/esmterrain/storage.cpp | 6 +++--- components/files/configurationmanager.cpp | 2 +- components/lua/asyncpackage.cpp | 2 +- components/lua_ui/element.cpp | 2 +- components/lua_ui/scriptsettings.cpp | 2 +- components/nif/niffile.cpp | 2 +- components/settings/parser.cpp | 2 +- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/components/bsa/ba2dx10file.cpp b/components/bsa/ba2dx10file.cpp index 945e8dd931..593ca64949 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); 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 980281240d..ec6313d6f1 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -601,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/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/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/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 84383f89e1..e993fba9fd 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 e92d1d8958..d552b7b3d6 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/nif/niffile.cpp b/components/nif/niffile.cpp index d6d063a254..7b20637c70 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -672,7 +672,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/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)); } From 8af8f331cb67d7a49c44a494aa2bb07d710d3f2b Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 19 Jan 2024 18:19:01 +0400 Subject: [PATCH 39/59] Avoid possible race in videoplayer --- extern/osg-ffmpeg-videoplayer/videostate.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 From 1f416d7c8ae0ec882c99b554619b4e33d1a40989 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Sat, 20 Jan 2024 11:56:36 +0000 Subject: [PATCH 40/59] Lua: Creature skill bindings --- apps/openmw/mwlua/types/creature.cpp | 15 +++++++++++++++ files/lua_api/openmw/types.lua | 9 +++++++++ 2 files changed, 24 insertions(+) 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/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index ad30994fe8..ba0b2dd58b 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. From 251d01304fc1aa7bd0f199a9636214bd8afcc00a Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 21 Jan 2024 13:48:33 +0400 Subject: [PATCH 41/59] Use move semantics for osg::ref_ptr --- apps/opencs/view/render/worldspacewidget.cpp | 2 +- apps/openmw/engine.cpp | 2 +- apps/openmw/mwrender/actoranimation.cpp | 2 +- apps/openmw/mwrender/globalmap.cpp | 7 ++++--- apps/openmw/mwrender/luminancecalculator.cpp | 2 +- apps/openmw/mwrender/objects.cpp | 2 +- apps/openmw/mwrender/ripples.cpp | 2 +- apps/openmw/mwrender/sky.cpp | 4 ++-- apps/openmw/mwrender/water.cpp | 4 ++-- components/nifosg/nifloader.cpp | 2 +- components/resource/keyframemanager.cpp | 4 ++-- components/sceneutil/util.cpp | 2 +- components/shader/shadervisitor.cpp | 10 +++++----- 13 files changed, 23 insertions(+), 22 deletions(-) diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index f7732d752d..2af84fb36d 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -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]; diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 3f3d5fe558..dfc11c309b 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -823,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/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/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index ac7a8a9351..e58f987a44 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -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/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/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..060b6ee5de 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -764,7 +764,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 +786,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/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/keyframemanager.cpp b/components/resource/keyframemanager.cpp index d60129cb86..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(); @@ -239,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/sceneutil/util.cpp b/components/sceneutil/util.cpp index ce48702a74..ab600de11d 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -248,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/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)); } } From e01e2f1ae0ce45240e8f4129998d8fa3c999c156 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 21 Jan 2024 17:50:45 +0400 Subject: [PATCH 42/59] Fix magic effects in the editor --- apps/opencs/model/world/collection.hpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 9db6b3b042..c10266a101 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)); From 737d3b499b90290e69365d0e8306a657ef64e4f7 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 21 Jan 2024 20:20:37 +0400 Subject: [PATCH 43/59] Use move semantics for tools --- apps/essimporter/convertacdt.cpp | 2 +- apps/essimporter/converter.cpp | 6 +++--- apps/opencs/model/filter/parser.cpp | 2 +- apps/opencs/model/tools/reportmodel.cpp | 2 +- apps/opencs/model/world/actoradapter.cpp | 8 ++++---- apps/opencs/model/world/collection.hpp | 2 +- apps/opencs/view/world/tablesubview.cpp | 2 +- apps/wizard/unshield/unshieldworker.cpp | 7 +++---- 8 files changed, 15 insertions(+), 16 deletions(-) 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/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..dbbff2ed4a 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -504,7 +504,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/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index 1d4dc37529..891d954ad4 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -195,7 +195,7 @@ void CSVWorld::TableSubView::createFilterRequest(std::vector Date: Sun, 21 Jan 2024 12:54:33 +0400 Subject: [PATCH 44/59] Fix error message about savegame format --- apps/openmw/mwstate/statemanagerimp.cpp | 99 +++++++++++++++++-------- apps/openmw/mwstate/statemanagerimp.hpp | 2 + 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 631ef9a112..8d3b84df13 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -409,9 +409,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) @@ -427,23 +456,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); @@ -625,23 +640,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 a76b829e38..c25e43cc23 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -22,6 +22,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; From 84ab7afd444f37e122e250ac4ca2e922472c42f2 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 21 Jan 2024 22:54:54 +0300 Subject: [PATCH 45/59] Make BA2 extension hash calculation safer (#7784) --- components/bsa/ba2dx10file.cpp | 2 +- components/bsa/ba2file.cpp | 8 ++++++++ components/bsa/ba2file.hpp | 1 + components/bsa/ba2gnrlfile.cpp | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/components/bsa/ba2dx10file.cpp b/components/bsa/ba2dx10file.cpp index 593ca64949..aa3f8d0581 100644 --- a/components/bsa/ba2dx10file.cpp +++ b/components/bsa/ba2dx10file.cpp @@ -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 From 0ea88df46dd7ceb7b7fb4477fd68db159792340c Mon Sep 17 00:00:00 2001 From: Diego Date: Sun, 21 Jan 2024 19:48:13 -0500 Subject: [PATCH 46/59] added instructions for extracting .esm files using innoextract --- .../installation/install-game-files.rst | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 57460c4983..1925d16d97 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -68,6 +68,51 @@ 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 +^^^^^^^^^^^ + +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, you can do so using ``innoextract``. + +For Distributions Using `apt` (e.g., Ubuntu, Debian) +++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. code:: bash + + sudo apt update + sudo apt install innoextract + +Other Distributions ++++++++++++++++++++ + +If you are using a Linux distribution that doesn't use apt, search your package manager for the program and install it if it exists. + +.. code:: bash + + # openSUSE + sudo zypper refresh + sudo zypper search innoextract + sudo zypper install innoextract + +.. code:: bash + + # Arch Linux/Manjaro + sudo pacman -Sy + sudo pacman -Ss innoextract + sudo pacman -S innoextract + +Once the program is installed, download the game from GOG. The file should be called ``setup_tes_morrowind_goty_2.0.0.7.exe`` or something similar. When you run ``innoextract`` 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:: bash + + innoextract setup_tes_morrowind_goty_2.0.0.7.exe -d ~/Documents/Games/Morrowind/ + +If not just run the command without the ``-d`` flag. Assuming you used the filepath above, your ``.esm`` files will be located in this diredctory ``~/Documents/Games/Morrowind/app/Data Files/``. + +Now you can run the OpenMW launcher and run the installation wizard. Point it to your ``Morrowind.esm`` in the folder you extracted it to, and enjoy playing Morrowind. + ----- Steam ----- From 0f3c4f2043ceca1a2585e1427de4e228a081a591 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Mon, 22 Jan 2024 12:41:53 +0000 Subject: [PATCH 47/59] Applying Feedback --- docs/source/manuals/installation/install-game-files.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 1925d16d97..57f8d65a88 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -74,7 +74,7 @@ Innoextract 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, you can do so using ``innoextract``. +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, you can do so using `innoextract `_. For Distributions Using `apt` (e.g., Ubuntu, Debian) ++++++++++++++++++++++++++++++++++++++++++++++++++++ From 6a1979e5f1ded54e9b40cde58071f9cca9613655 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Mon, 22 Jan 2024 12:42:42 +0000 Subject: [PATCH 48/59] Applying feedback to fix the verbiage of using innoextract --- docs/source/manuals/installation/install-game-files.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 57f8d65a88..21f01983f3 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -103,7 +103,7 @@ If you are using a Linux distribution that doesn't use apt, search your package sudo pacman -Ss innoextract sudo pacman -S innoextract -Once the program is installed, download the game from GOG. The file should be called ``setup_tes_morrowind_goty_2.0.0.7.exe`` or something similar. When you run ``innoextract`` 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. +Once the 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:: bash From d6d1cb099f3e4908bcf088f7e5cc312bc81dc430 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Mon, 22 Jan 2024 12:43:20 +0000 Subject: [PATCH 49/59] applying feedback to switch from bash to console --- docs/source/manuals/installation/install-game-files.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 21f01983f3..c8464c4f57 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -105,9 +105,9 @@ If you are using a Linux distribution that doesn't use apt, search your package Once the 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:: bash +.. code:: console - innoextract setup_tes_morrowind_goty_2.0.0.7.exe -d ~/Documents/Games/Morrowind/ + $ innoextract ./setup_tes_morrowind_goty_2.0.0.7.exe -d ~/Documents/Games/Morrowind/ If not just run the command without the ``-d`` flag. Assuming you used the filepath above, your ``.esm`` files will be located in this diredctory ``~/Documents/Games/Morrowind/app/Data Files/``. From a7473a2134e1fb43e641bcd93e3f4268b2c8b20c Mon Sep 17 00:00:00 2001 From: jvoisin Date: Mon, 22 Jan 2024 12:44:07 +0000 Subject: [PATCH 50/59] Applying feedback to remove redundant instructions --- docs/source/manuals/installation/install-game-files.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index c8464c4f57..5d5350bde2 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -109,7 +109,7 @@ Once the innoextract is installed, download the game from GOG. The downloaded fi $ innoextract ./setup_tes_morrowind_goty_2.0.0.7.exe -d ~/Documents/Games/Morrowind/ -If not just run the command without the ``-d`` flag. Assuming you used the filepath above, your ``.esm`` files will be located in this diredctory ``~/Documents/Games/Morrowind/app/Data Files/``. +Assuming you used the filepath above, your ``.esm`` files will be located in ``~/Documents/Games/Morrowind/app/Data Files/``. Now you can run the OpenMW launcher and run the installation wizard. Point it to your ``Morrowind.esm`` in the folder you extracted it to, and enjoy playing Morrowind. From 6029545bc7ff9806eedda2f5bf470f50051fe7ef Mon Sep 17 00:00:00 2001 From: jvoisin Date: Mon, 22 Jan 2024 12:45:01 +0000 Subject: [PATCH 51/59] Applying feedback to make verbiage less casual --- docs/source/manuals/installation/install-game-files.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 5d5350bde2..4980f902a7 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -111,7 +111,7 @@ Once the innoextract is installed, download the game from GOG. The downloaded fi Assuming you used the filepath above, your ``.esm`` files will be located in ``~/Documents/Games/Morrowind/app/Data Files/``. -Now you can run the OpenMW launcher and run the installation wizard. Point it to your ``Morrowind.esm`` in the folder you extracted it to, and enjoy playing Morrowind. +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 From 5c135551e611e274a798855c696f92551c8ead14 Mon Sep 17 00:00:00 2001 From: Diego Date: Mon, 22 Jan 2024 07:49:25 -0500 Subject: [PATCH 52/59] removed zypper and pacman commands --- .../installation/install-game-files.rst | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 4980f902a7..413061f8c4 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -79,31 +79,12 @@ If you have purchased "The Elder Scrolls III: Morrowind" `from GOG Date: Mon, 22 Jan 2024 14:33:52 +0000 Subject: [PATCH 53/59] added instructions to install innoextract using homebrew --- .../manuals/installation/install-game-files.rst | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 413061f8c4..6da5d3d55a 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -71,10 +71,10 @@ Users of other platforms running Wine, will find it at Innoextract ^^^^^^^^^^^ -Linux -~~~~~ +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, you can do so using `innoextract `_. +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) ++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -84,6 +84,13 @@ For Distributions Using `apt` (e.g., Ubuntu, Debian) 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 @@ -92,7 +99,7 @@ Once innoextract is installed, download the game from GOG. The downloaded file s 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. +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 From 9e55425b71cfd906294cabb8223b88926875f16b Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 24 Jan 2024 20:39:04 +0400 Subject: [PATCH 54/59] Use std::move() in /apps/openmw --- apps/openmw/mwclass/ingredient.cpp | 2 +- apps/openmw/mwgui/charactercreation.cpp | 12 ++++++------ apps/openmw/mwgui/dialogue.cpp | 4 ++-- apps/openmw/mwgui/postprocessorhud.cpp | 6 +++--- apps/openmw/mwlua/cellbindings.cpp | 2 +- apps/openmw/mwlua/objectbindings.cpp | 4 ++-- apps/openmw/mwmechanics/character.cpp | 6 +++--- apps/openmw/mwrender/characterpreview.cpp | 4 ++-- apps/openmw/mwrender/creatureanimation.cpp | 2 +- apps/openmw/mwrender/globalmap.cpp | 2 +- apps/openmw/mwrender/npcanimation.cpp | 2 +- apps/openmw/mwscript/miscextensions.cpp | 2 +- apps/openmw/mwsound/soundmanagerimp.cpp | 12 ++++++------ apps/openmw/mwworld/containerstore.cpp | 2 +- apps/openmw/mwworld/store.cpp | 2 +- 15 files changed, 32 insertions(+), 32 deletions(-) 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/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 ce79e2834c..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()); } } 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/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp index 081df13a0e..39dad5a867 100644 --- a/apps/openmw/mwlua/cellbindings.cpp +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -270,7 +270,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/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/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 20c7fd0a92..f1bec8ce9d 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -684,7 +684,7 @@ namespace MWMechanics if (!mAnimation->hasAnimation(weapMovementAnimName)) weapMovementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); - movementAnimName = weapMovementAnimName; + movementAnimName = std::move(weapMovementAnimName); } if (!mAnimation->hasAnimation(movementAnimName)) @@ -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,7 +820,7 @@ namespace MWMechanics mAnimation->getInfo(mCurrentIdle, &startPoint); clearStateAnimation(mCurrentIdle); - mCurrentIdle = idleGroup; + mCurrentIdle = std::move(idleGroup); mAnimation->play(mCurrentIdle, priority, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true); } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 88ceeabd23..269f7cab75 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,7 +456,7 @@ namespace MWRender mAnimation->showCarriedLeft(showCarriedLeft); - mCurrentAnimGroup = groupname; + mCurrentAnimGroup = std::move(groupname); mAnimation->play(mCurrentAnimGroup, 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); 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 e58f987a44..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 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/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/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 383d316c91..0cc276807f 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -223,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 { @@ -236,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; @@ -282,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) @@ -366,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 @@ -393,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; @@ -422,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/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); } From 29a40c212fe58a753307d9802b05cd3a6b6266c1 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Thu, 25 Jan 2024 15:40:49 +0300 Subject: [PATCH 55/59] Support parsing KF files in niftest Mention BA2 support in help messages --- apps/niftest/niftest.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index 9352651030..a0dbb1233b 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -34,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) @@ -76,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; @@ -139,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)"); @@ -241,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) From 3b0d654a3f7ea77fa78526a3cf031badee3e0db4 Mon Sep 17 00:00:00 2001 From: Pharis Date: Thu, 25 Jan 2024 18:06:14 -0600 Subject: [PATCH 56/59] Return active spell id from pairs --- apps/openmw/mwlua/magicbindings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 3d57ab24fc..6e35776cba 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -730,7 +730,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 { From daa9c5f0e111f9938e9b2066ac38b7d8f73e8bac Mon Sep 17 00:00:00 2001 From: Zackhasacat Date: Fri, 26 Jan 2024 20:37:04 +0000 Subject: [PATCH 57/59] Lua: Add water level to Core.Cell --- apps/openmw/mwlua/cellbindings.cpp | 7 +++++++ files/lua_api/openmw/core.lua | 1 + 2 files changed, 8 insertions(+) diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp index 081df13a0e..8eb08dbbd0 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())]( diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 7ea3c75f1c..dae5fc0594 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. --- From a94add741e416feb7ddbde9348fca405f57939b3 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Fri, 26 Jan 2024 21:39:33 +0000 Subject: [PATCH 58/59] Lua: Animation bindings --- apps/openmw/CMakeLists.txt | 6 +- apps/openmw/mwbase/luamanager.hpp | 7 +- apps/openmw/mwbase/mechanicsmanager.hpp | 29 +- apps/openmw/mwlua/animationbindings.cpp | 365 ++++++++++++++++++ apps/openmw/mwlua/animationbindings.hpp | 12 + apps/openmw/mwlua/engineevents.cpp | 9 + apps/openmw/mwlua/engineevents.hpp | 9 +- apps/openmw/mwlua/localscripts.cpp | 8 +- apps/openmw/mwlua/localscripts.hpp | 10 + apps/openmw/mwlua/luabindings.cpp | 3 + apps/openmw/mwlua/luamanagerimp.cpp | 44 +++ apps/openmw/mwlua/luamanagerimp.hpp | 4 + apps/openmw/mwlua/magicbindings.cpp | 11 + apps/openmw/mwmechanics/activespells.cpp | 2 +- apps/openmw/mwmechanics/actors.cpp | 25 ++ apps/openmw/mwmechanics/actors.hpp | 4 + apps/openmw/mwmechanics/character.cpp | 229 ++++++----- apps/openmw/mwmechanics/character.hpp | 15 +- .../mwmechanics/mechanicsmanagerimp.cpp | 23 ++ .../mwmechanics/mechanicsmanagerimp.hpp | 4 + apps/openmw/mwmechanics/objects.cpp | 24 ++ apps/openmw/mwmechanics/objects.hpp | 4 + apps/openmw/mwmechanics/spellcasting.cpp | 8 +- apps/openmw/mwmechanics/spelleffects.cpp | 14 +- apps/openmw/mwmechanics/summoning.cpp | 2 +- apps/openmw/mwrender/animation.cpp | 95 +++-- apps/openmw/mwrender/animation.hpp | 78 +--- apps/openmw/mwrender/animationpriority.hpp | 42 ++ apps/openmw/mwrender/blendmask.hpp | 22 ++ apps/openmw/mwrender/bonegroup.hpp | 16 + apps/openmw/mwrender/characterpreview.cpp | 6 +- apps/openmw/mwrender/sky.cpp | 11 +- components/esm3/loadmgef.cpp | 2 +- components/resource/scenemanager.cpp | 6 +- components/resource/scenemanager.hpp | 6 +- docs/source/luadoc_data_paths.sh | 1 + docs/source/reference/lua-scripting/api.rst | 2 + .../lua-scripting/interface_animation.rst | 8 + .../lua-scripting/openmw_animation.rst | 7 + .../lua-scripting/tables/interfaces.rst | 3 + .../lua-scripting/tables/packages.rst | 2 + files/data/CMakeLists.txt | 1 + files/data/builtin.omwscripts | 1 + .../omw/mechanics/animationcontroller.lua | 145 +++++++ files/lua_api/openmw/animation.lua | 255 ++++++++++++ files/lua_api/openmw/core.lua | 25 ++ files/lua_api/openmw/interfaces.lua | 3 + 47 files changed, 1380 insertions(+), 228 deletions(-) create mode 100644 apps/openmw/mwlua/animationbindings.cpp create mode 100644 apps/openmw/mwlua/animationbindings.hpp create mode 100644 apps/openmw/mwrender/animationpriority.hpp create mode 100644 apps/openmw/mwrender/blendmask.hpp create mode 100644 apps/openmw/mwrender/bonegroup.hpp create mode 100644 docs/source/reference/lua-scripting/interface_animation.rst create mode 100644 docs/source/reference/lua-scripting/openmw_animation.rst create mode 100644 files/data/scripts/omw/mechanics/animationcontroller.lua create mode 100644 files/lua_api/openmw/animation.lua diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 373de3683d..566aedfff0 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -24,7 +24,7 @@ add_openmw_dir (mwrender bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass precipitationocclusion ripples - actorutil distortion + actorutil distortion animationpriority bonegroup blendmask ) add_openmw_dir (mwinput @@ -64,7 +64,7 @@ add_openmw_dir (mwlua context globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings itemdata types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/clothing types/levelledlist types/terminal - worker magicbindings factionbindings classbindings + worker magicbindings factionbindings classbindings animationbindings ) add_openmw_dir (mwsound @@ -109,7 +109,7 @@ add_openmw_dir (mwstate add_openmw_dir (mwbase environment world scriptmanager dialoguemanager journal soundmanager mechanicsmanager - inputmanager windowmanager statemanager + inputmanager windowmanager statemanager luamanager ) # Main executable diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index f3cea83224..0503fcec9e 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 @@ -60,10 +61,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 9e99a37ec7..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 @@ -192,6 +210,9 @@ namespace MWBase /// 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/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/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/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 48f2f3e35d..4da34bf9d4 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -35,6 +35,7 @@ #include "mwscriptbindings.hpp" #include "objectlists.hpp" +#include "animationbindings.hpp" #include "camerabindings.hpp" #include "cellbindings.hpp" #include "debugbindings.hpp" @@ -147,6 +148,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) @@ -330,6 +332,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 2417e9e340..89402a6008 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -23,6 +23,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwrender/bonegroup.hpp" #include "../mwrender/postprocessor.hpp" #include "../mwworld/datetimemanager.hpp" @@ -362,6 +363,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 8bd189d8e9..7556abad5d 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -79,6 +79,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 6e35776cba..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() 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 bc3cc3bb6a..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); @@ -2048,6 +2066,13 @@ namespace MWMechanics 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 7c676ca018..3ead5f069a 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -114,10 +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/character.cpp b/apps/openmw/mwmechanics/character.cpp index f1bec8ce9d..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); @@ -749,7 +749,7 @@ namespace MWMechanics } } - mAnimation->play( + playBlendedAnimation( mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true); } @@ -821,8 +821,8 @@ namespace MWMechanics clearStateAnimation(mCurrentIdle); mCurrentIdle = std::move(idleGroup); - mAnimation->play(mCurrentIdle, priority, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", - startPoint, numLoops, true); + 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 dc551900b5..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,8 +213,6 @@ 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; @@ -247,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(); @@ -274,10 +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/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index c9e8e8f322..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()) @@ -799,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 f2483396fe..93c1fa3dc2 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -143,10 +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/animation.cpp b/apps/openmw/mwrender/animation.cpp index feed9719b6..581d2843ab 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -46,6 +46,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" @@ -53,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" @@ -301,11 +303,10 @@ namespace RemoveCallbackVisitor() : RemoveVisitor() , mHasMagicEffects(false) - , mEffectId(-1) { } - RemoveCallbackVisitor(int effectId) + RemoveCallbackVisitor(std::string_view effectId) : RemoveVisitor() , mHasMagicEffects(false) , mEffectId(effectId) @@ -324,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 @@ -338,7 +339,7 @@ namespace void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; class FindVfxCallbacksVisitor : public osg::NodeVisitor @@ -348,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) { @@ -368,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); } @@ -382,7 +382,7 @@ namespace void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; osg::ref_ptr getVFXLightModelInstance() @@ -447,7 +447,7 @@ namespace MWRender typedef std::map> ControllerMap; - ControllerMap mControllerMap[Animation::sNumBlendMasks]; + ControllerMap mControllerMap[sNumBlendMasks]; const SceneUtil::TextKeyMap& getTextKeys() const; }; @@ -715,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) @@ -758,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(); } } @@ -923,7 +956,7 @@ namespace MWRender return true; } - void Animation::setTextKeyListener(Animation::TextKeyListener* listener) + void Animation::setTextKeyListener(TextKeyListener* listener) { mTextKeyListener = listener; } @@ -1052,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()) @@ -1496,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; @@ -1579,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); @@ -1589,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) { @@ -1608,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 269f7cab75..aa6b5eb4dd 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -457,14 +457,14 @@ namespace MWRender mAnimation->showCarriedLeft(showCarriedLeft); mCurrentAnimGroup = std::move(groupname); - mAnimation->play(mCurrentAnimGroup, 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); + 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/sky.cpp b/apps/openmw/mwrender/sky.cpp index 060b6ee5de..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); 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/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/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 7bcda5110c..02b03cbd69 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -2,6 +2,7 @@ paths=( openmw_aux/*lua scripts/omw/activationhandlers.lua scripts/omw/ai.lua + scripts/omw/mechanics/animationcontroller.lua scripts/omw/playercontrols.lua scripts/omw/camera/camera.lua scripts/omw/mwui/init.lua diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 6d27db0515..1bb7e0b6e9 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 @@ -33,6 +34,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 67709bbf7b..9a73334b84 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/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 0e91a0b495..3ab30c87ff 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -75,6 +75,7 @@ set(BUILTIN_DATA_FILES scripts/omw/console/player.lua scripts/omw/console/global.lua scripts/omw/console/local.lua + scripts/omw/mechanics/animationcontroller.lua scripts/omw/mechanics/playercontroller.lua scripts/omw/playercontrols.lua scripts/omw/settings/player.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index e4338df533..a6f4ca5f33 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -10,6 +10,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 PLAYER: scripts/omw/playercontrols.lua PLAYER: scripts/omw/camera/camera.lua 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/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 dae5fc0594..94d63312f1 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -668,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 @@ -899,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 From cfd67f3ce7577a48092bba98634a54dbceb7310c Mon Sep 17 00:00:00 2001 From: Zackhasacat Date: Sat, 27 Jan 2024 13:57:26 +0000 Subject: [PATCH 59/59] #7791: Require local variables to exist for lua mwscript local variables --- apps/openmw/mwlua/mwscriptbindings.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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();