From 4a3ffb2073451a50b1505809625e820cd4b3510c Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 17:44:25 +0200 Subject: [PATCH 01/33] Use camel case for variables --- apps/openmw/mwmechanics/aiwander.cpp | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 3cc7aac838..447140d8e3 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -29,17 +29,6 @@ namespace MWMechanics { - static const int COUNT_BEFORE_RESET = 10; - static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f; - - // to prevent overcrowding - static const int DESTINATION_TOLERANCE = 64; - - // distance must be long enough that NPC will need to move to get there. - static const int MINIMUM_WANDER_DISTANCE = DESTINATION_TOLERANCE * 2; - - static const std::size_t MAX_IDLE_SIZE = 8; - const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { "idle2", "idle3", @@ -53,11 +42,22 @@ namespace MWMechanics namespace { + constexpr int countBeforeReset = 10; + constexpr float idlePositionCheckInterval = 1.5f; + + // to prevent overcrowding + constexpr int destinationTolerance = 64; + + // distance must be long enough that NPC will need to move to get there. + constexpr int minimumWanderDistance = destinationTolerance * 2; + + constexpr std::size_t maxIdleSize = 8; + inline int getCountBeforeReset(const MWWorld::ConstPtr& actor) { if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor)) return 1; - return COUNT_BEFORE_RESET; + return countBeforeReset; } osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) @@ -99,12 +99,12 @@ namespace MWMechanics std::vector getInitialIdle(const std::vector& idle) { - std::vector result(MAX_IDLE_SIZE, 0); - std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin()); + std::vector result(maxIdleSize, 0); + std::copy_n(idle.begin(), std::min(maxIdleSize, idle.size()), result.begin()); return result; } - std::vector getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE]) + std::vector getInitialIdle(const unsigned char (&idle)[maxIdleSize]) { return std::vector(std::begin(idle), std::end(idle)); } @@ -494,7 +494,7 @@ namespace MWMechanics // Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. storage.mCheckIdlePositionTimer += duration; - if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) + if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary()) { storage.mCheckIdlePositionTimer = 0; // restart timer static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; @@ -535,7 +535,7 @@ namespace MWMechanics // Is there no destination or are we there yet? if ((!mPathFinder.isPathConstructed()) || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections, - DESTINATION_TOLERANCE)) + destinationTolerance)) { stopWalking(actor); storage.setState(AiWanderStorage::Wander_ChooseAction); @@ -915,7 +915,7 @@ namespace MWMechanics float length = delta.length(); delta.normalize(); - int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE); + int distance = std::max(mDistance / 2, minimumWanderDistance); // must not travel longer than distance between waypoints or NPC goes past waypoint distance = std::min(distance, static_cast(length)); From 20bd1491a784b19dba1b2f22cb473555a2ebbfea Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 17:38:51 +0200 Subject: [PATCH 02/33] Make sure distance and duration are not negative --- apps/openmw/mwmechanics/aiwander.cpp | 19 ++++++++----------- apps/openmw/mwmechanics/aiwander.hpp | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 447140d8e3..3dacbd167e 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -46,10 +46,10 @@ namespace MWMechanics constexpr float idlePositionCheckInterval = 1.5f; // to prevent overcrowding - constexpr int destinationTolerance = 64; + constexpr unsigned destinationTolerance = 64; // distance must be long enough that NPC will need to move to get there. - constexpr int minimumWanderDistance = destinationTolerance * 2; + constexpr unsigned minimumWanderDistance = destinationTolerance * 2; constexpr std::size_t maxIdleSize = 8; @@ -128,8 +128,8 @@ namespace MWMechanics AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat) : TypedAiPackage(repeat) - , mDistance(std::max(0, distance)) - , mDuration(std::max(0, duration)) + , mDistance(static_cast(std::max(0, distance))) + , mDuration(static_cast(std::max(0, duration))) , mRemainingDuration(duration) , mTimeOfDay(timeOfDay) , mIdle(getInitialIdle(idle)) @@ -259,9 +259,6 @@ namespace MWMechanics bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) { - if (mDistance <= 0) - storage.mCanWanderAlongPathGrid = false; - if (isPackageCompleted()) { stopWalking(actor); @@ -915,10 +912,10 @@ namespace MWMechanics float length = delta.length(); delta.normalize(); - int distance = std::max(mDistance / 2, minimumWanderDistance); + unsigned distance = std::max(mDistance / 2, minimumWanderDistance); // must not travel longer than distance between waypoints or NPC goes past waypoint - distance = std::min(distance, static_cast(length)); + distance = std::min(distance, static_cast(length)); delta *= distance; storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); } @@ -970,8 +967,8 @@ namespace MWMechanics AiWander::AiWander(const ESM::AiSequence::AiWander* wander) : TypedAiPackage(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0)) - , mDistance(std::max(static_cast(0), wander->mData.mDistance)) - , mDuration(std::max(static_cast(0), wander->mData.mDuration)) + , mDistance(static_cast(std::max(static_cast(0), wander->mData.mDistance))) + , mDuration(static_cast(std::max(static_cast(0), wander->mData.mDuration))) , mRemainingDuration(wander->mDurationData.mRemainingDuration) , mTimeOfDay(wander->mData.mTimeOfDay) , mIdle(getInitialIdle(wander->mData.mIdle)) diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index f08980ad29..a1474bce5f 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -147,8 +147,8 @@ namespace MWMechanics void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); bool isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; - const int mDistance; // how far the actor can wander from the spawn point - const int mDuration; + const unsigned mDistance; // how far the actor can wander from the spawn point + const unsigned mDuration; float mRemainingDuration; const int mTimeOfDay; const std::vector mIdle; From c79b39cf0d59e5c8c119e04daa4056467bb2de6a Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 12:41:10 +0200 Subject: [PATCH 03/33] Remove unused arguments from isAreaOccupiedByOtherActor --- apps/openmw/mwbase/world.hpp | 4 ++-- apps/openmw/mwmechanics/obstacle.cpp | 10 ++-------- apps/openmw/mwmechanics/obstacle.hpp | 5 +---- .../mwphysics/hasspherecollisioncallback.hpp | 17 +++++------------ apps/openmw/mwphysics/physicssystem.cpp | 17 +++-------------- apps/openmw/mwphysics/physicssystem.hpp | 4 ++-- apps/openmw/mwworld/worldimp.cpp | 6 +++--- apps/openmw/mwworld/worldimp.hpp | 4 ++-- 8 files changed, 20 insertions(+), 47 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index f268ed0e52..9016e23c0e 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -574,8 +574,8 @@ namespace MWBase virtual bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; - virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors = nullptr) const = 0; + virtual bool isAreaOccupiedByOtherActor( + const osg::Vec3f& position, float radius, std::span ignore) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index a6eb4f9c24..1112bc49d6 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -106,19 +106,13 @@ namespace MWMechanics return visitor.mResult; } - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer, - std::vector* occupyingActors) + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination) { const auto world = MWBase::Environment::get().getWorld(); const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents; const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - if (ignorePlayer) - { - const std::array ignore{ actor, world->getPlayerConstPtr() }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); - } const std::array ignore{ actor }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); + return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore); } ObstacleCheck::ObstacleCheck() diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index 532bc91331..f3214210b5 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -5,8 +5,6 @@ #include -#include - namespace MWWorld { class Ptr; @@ -24,8 +22,7 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, - bool ignorePlayer = false, std::vector* occupyingActors = nullptr); + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination); class ObstacleCheck { diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index c1fa0ad1ee..a6563c731a 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -10,7 +10,7 @@ namespace MWPhysics { // https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection - bool testAabbAgainstSphere( + inline bool testAabbAgainstSphere( const btVector3& aabbMin, const btVector3& aabbMax, const btVector3& position, const btScalar radius) { const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()), @@ -18,35 +18,29 @@ namespace MWPhysics return nearest.distance(position) < radius; } - template + template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: - HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group, - const Ignore& ignore, OnCollision* onCollision) + explicit HasSphereCollisionCallback( + const btVector3& position, const btScalar radius, const int mask, const int group, const Ignore& ignore) : mPosition(position) , mRadius(radius) , mIgnore(ignore) , mCollisionFilterMask(mask) , mCollisionFilterGroup(group) - , mOnCollision(onCollision) { } bool process(const btBroadphaseProxy* proxy) override { - if (mResult && mOnCollision == nullptr) + if (mResult) return false; const auto collisionObject = static_cast(proxy->m_clientObject); if (mIgnore(collisionObject) || !needsCollision(*proxy) || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; mResult = true; - if (mOnCollision != nullptr) - { - (*mOnCollision)(collisionObject); - return true; - } return !mResult; } @@ -58,7 +52,6 @@ namespace MWPhysics Ignore mIgnore; int mCollisionFilterMask; int mCollisionFilterGroup; - OnCollision* mOnCollision; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 5e7c70788d..55a0453bf8 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -849,8 +849,8 @@ namespace MWPhysics mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile); } - bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const + bool PhysicsSystem::isAreaOccupiedByOtherActor( + const osg::Vec3f& position, const float radius, std::span ignore) const { std::vector ignoredObjects; ignoredObjects.reserve(ignore.size()); @@ -867,18 +867,7 @@ namespace MWPhysics const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; const int group = MWPhysics::CollisionType_AnyPhysical; - if (occupyingActors == nullptr) - { - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, - static_cast(nullptr)); - mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); - return callback.getResult(); - } - const auto onCollision = [&](const btCollisionObject* object) { - if (PtrHolder* holder = static_cast(object->getUserPointer())) - occupyingActors->push_back(holder->getPtr()); - }; - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, &onCollision); + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 546d72676e..92b37e105a 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -281,8 +281,8 @@ namespace MWPhysics std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); } - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const; + bool isAreaOccupiedByOtherActor( + const osg::Vec3f& position, float radius, std::span ignore) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 97788669d5..099c6e403a 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3832,10 +3832,10 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } - bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const + bool World::isAreaOccupiedByOtherActor( + const osg::Vec3f& position, const float radius, std::span ignore) const { - return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore, occupyingActors); + return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore); } void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index b1286d5532..cce9d45e55 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -656,8 +656,8 @@ namespace MWWorld bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const override; + bool isAreaOccupiedByOtherActor( + const osg::Vec3f& position, float radius, std::span ignore) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; From ae909d7685febf8ecee6ac3ed823f19a3bcf4073 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 12:54:07 +0200 Subject: [PATCH 04/33] Remove isAreaOccupiedByOtherActor from obstacle.* It uses functions only from World anyway. --- apps/openmw/mwbase/world.hpp | 3 +-- apps/openmw/mwmechanics/aiwander.cpp | 16 +++++++------ apps/openmw/mwmechanics/obstacle.cpp | 9 -------- apps/openmw/mwmechanics/obstacle.hpp | 2 -- .../mwphysics/hasspherecollisioncallback.hpp | 9 ++++---- apps/openmw/mwphysics/physicssystem.cpp | 23 +++++++------------ apps/openmw/mwphysics/physicssystem.hpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 7 +++--- apps/openmw/mwworld/worldimp.hpp | 3 +-- 9 files changed, 28 insertions(+), 46 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 9016e23c0e..fea90171ed 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -574,8 +574,7 @@ namespace MWBase virtual bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; - virtual bool isAreaOccupiedByOtherActor( - const osg::Vec3f& position, float radius, std::span ignore) const = 0; + virtual bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 3dacbd167e..5112979c88 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -279,7 +279,9 @@ namespace MWMechanics getAllowedNodes(actor, storage); } - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + + auto& prng = world.getPrng(); if (canActorMoveByZAxis(actor) && mDistance > 0) { // Typically want to idle for a short time before the next wander @@ -333,7 +335,7 @@ namespace MWMechanics if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking && (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back()) - || isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) + || world.isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) completeManualWalking(actor, storage); return false; // AiWander package not yet completed @@ -363,12 +365,12 @@ namespace MWMechanics std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); - const auto world = MWBase::Environment::get().getWorld(); - const auto agentBounds = world->getPathfindingAgentBounds(actor); - const auto navigator = world->getNavigator(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + const auto agentBounds = world.getPathfindingAgentBounds(actor); + const auto navigator = world.getNavigator(); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + Misc::Rng::Generator& prng = world.getPrng(); do { @@ -408,7 +410,7 @@ namespace MWMechanics if (isDestinationHidden(actor, mDestination)) continue; - if (isAreaOccupiedByOtherActor(actor, mDestination)) + if (world.isAreaOccupiedByOtherActor(actor, mDestination)) continue; constexpr float endTolerance = 0; diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 1112bc49d6..9a66eafb51 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -106,15 +106,6 @@ namespace MWMechanics return visitor.mResult; } - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination) - { - const auto world = MWBase::Environment::get().getWorld(); - const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents; - const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - const std::array ignore{ actor }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore); - } - ObstacleCheck::ObstacleCheck() : mEvadeDirectionIndex(std::size(evadeDirections) - 1) { diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index f3214210b5..a1c973765f 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -22,8 +22,6 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination); - class ObstacleCheck { public: diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index a6563c731a..6270cd3083 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -18,12 +18,11 @@ namespace MWPhysics return nearest.distance(position) < radius; } - template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: - explicit HasSphereCollisionCallback( - const btVector3& position, const btScalar radius, const int mask, const int group, const Ignore& ignore) + explicit HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, + const int group, const btCollisionObject* ignore) : mPosition(position) , mRadius(radius) , mIgnore(ignore) @@ -37,7 +36,7 @@ namespace MWPhysics if (mResult) return false; const auto collisionObject = static_cast(proxy->m_clientObject); - if (mIgnore(collisionObject) || !needsCollision(*proxy) + if (mIgnore == collisionObject || !needsCollision(*proxy) || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; mResult = true; @@ -49,7 +48,7 @@ namespace MWPhysics private: btVector3 mPosition; btScalar mRadius; - Ignore mIgnore; + const btCollisionObject* mIgnore; int mCollisionFilterMask; int mCollisionFilterGroup; bool mResult = false; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 55a0453bf8..f403f97c2f 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -850,24 +850,17 @@ namespace MWPhysics } bool PhysicsSystem::isAreaOccupiedByOtherActor( - const osg::Vec3f& position, const float radius, std::span ignore) const + const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, const float radius) const { - std::vector ignoredObjects; - ignoredObjects.reserve(ignore.size()); - for (const auto& v : ignore) - if (const auto it = mActors.find(v.mRef); it != mActors.end()) - ignoredObjects.push_back(it->second->getCollisionObject()); - std::sort(ignoredObjects.begin(), ignoredObjects.end()); - ignoredObjects.erase(std::unique(ignoredObjects.begin(), ignoredObjects.end()), ignoredObjects.end()); - const auto ignoreFilter = [&](const btCollisionObject* v) { - return std::binary_search(ignoredObjects.begin(), ignoredObjects.end(), v); - }; - const auto bulletPosition = Misc::Convert::toBullet(position); - const auto aabbMin = bulletPosition - btVector3(radius, radius, radius); - const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); + const btCollisionObject* ignoredObject = nullptr; + if (const auto it = mActors.find(actor); it != mActors.end()) + ignoredObject = it->second->getCollisionObject(); + const btVector3 bulletPosition = Misc::Convert::toBullet(position); + const btVector3 aabbMin = bulletPosition - btVector3(radius, radius, radius); + const btVector3 aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; const int group = MWPhysics::CollisionType_AnyPhysical; - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter); + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoredObject); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 92b37e105a..8a845b4c41 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -282,7 +282,7 @@ namespace MWPhysics } bool isAreaOccupiedByOtherActor( - const osg::Vec3f& position, float radius, std::span ignore) const; + const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, float radius) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 099c6e403a..c8564a07b5 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3832,10 +3832,11 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } - bool World::isAreaOccupiedByOtherActor( - const osg::Vec3f& position, const float radius, std::span ignore) const + bool World::isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const { - return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore); + const osg::Vec3f halfExtents = getPathfindingAgentBounds(actor).mHalfExtents; + const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + return mPhysics->isAreaOccupiedByOtherActor(actor.mRef, position, 2 * maxHalfExtent); } void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index cce9d45e55..ac5e315a58 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -656,8 +656,7 @@ namespace MWWorld bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; - bool isAreaOccupiedByOtherActor( - const osg::Vec3f& position, float radius, std::span ignore) const override; + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; From 7c46635a5a9526713be8d9aa8ebb60460fab7835 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 13:59:36 +0200 Subject: [PATCH 05/33] Make trimAllowedNodes a free function --- apps/openmw/mwmechanics/aiwander.cpp | 52 ++++++++++++++-------------- apps/openmw/mwmechanics/aiwander.hpp | 2 -- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 5112979c88..2bdeb27179 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -109,6 +109,31 @@ namespace MWMechanics return std::vector(std::begin(idle), std::end(idle)); } + void trimAllowedNodes(const std::deque& path, std::vector& nodes) + { + // TODO: how to add these back in once the door opens? + // Idea: keep a list of detected closed doors (see aicombat.cpp) + // Every now and then check whether one of the doors is opened. (maybe + // at the end of playing idle?) If the door is opened then re-calculate + // allowed nodes starting from the spawn point. + std::vector points(path.begin(), path.end()); + while (points.size() >= 2) + { + const osg::Vec3f point = points.back(); + for (std::size_t j = 0; j < nodes.size(); j++) + { + // FIXME: doesn't handle a door with the same X/Y + // coordinates but with a different Z + if (std::abs(nodes[j].mX - point.x()) <= 0.5 && std::abs(nodes[j].mY - point.y()) <= 0.5) + { + nodes.erase(nodes.begin() + j); + break; + } + } + points.pop_back(); + } + } + } AiWanderStorage::AiWanderStorage() @@ -586,7 +611,7 @@ namespace MWMechanics { // remove allowed points then select another random destination storage.mTrimCurrentNode = true; - trimAllowedNodes(storage.mAllowedNodes, mPathFinder); + trimAllowedNodes(mPathFinder.getPath(), storage.mAllowedNodes); mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_MoveNow); @@ -643,31 +668,6 @@ namespace MWMechanics storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); } - void AiWander::trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder) - { - // TODO: how to add these back in once the door opens? - // Idea: keep a list of detected closed doors (see aicombat.cpp) - // Every now and then check whether one of the doors is opened. (maybe - // at the end of playing idle?) If the door is opened then re-calculate - // allowed nodes starting from the spawn point. - auto paths = pathfinder.getPath(); - while (paths.size() >= 2) - { - const auto pt = paths.back(); - for (unsigned int j = 0; j < nodes.size(); j++) - { - // FIXME: doesn't handle a door with the same X/Y - // coordinates but with a different Z - if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5) - { - nodes.erase(nodes.begin() + j); - break; - } - } - paths.pop_back(); - } - } - void AiWander::stopWalking(const MWWorld::Ptr& actor) { mPathFinder.clearPath(); diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index a1474bce5f..0bb0f64a83 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -166,8 +166,6 @@ namespace MWMechanics void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage); - void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); - // constants for converting idleSelect values into groupNames enum GroupIndex { From 2bce45260ccca602db2eba8853dca1d810f0e805 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 5 Jul 2025 14:06:56 +0200 Subject: [PATCH 06/33] Drop support for Qt5 --- .gitlab-ci.yml | 56 +++++++++---------- CI/before_script.msvc.sh | 13 ++--- CI/install_debian_deps.sh | 30 +++++----- CMakeLists.txt | 8 +-- apps/launcher/importpage.cpp | 4 -- apps/launcher/maindialog.cpp | 4 -- apps/opencs/CMakeLists.txt | 6 +- apps/opencs/model/filter/textnode.cpp | 6 +- apps/opencs/model/filter/valuenode.cpp | 4 +- .../model/prefs/shortcuteventhandler.cpp | 35 +++++------- .../model/prefs/shortcuteventhandler.hpp | 6 +- apps/opencs/model/prefs/shortcutmanager.cpp | 9 +-- apps/opencs/model/prefs/state.cpp | 7 --- apps/opencs/view/doc/view.cpp | 4 -- apps/opencs/view/filter/editwidget.hpp | 2 +- apps/opencs/view/render/instancemode.cpp | 2 +- apps/opencs/view/render/scenewidget.cpp | 8 +-- apps/opencs/view/render/scenewidget.hpp | 2 +- apps/opencs/view/render/terrainshapemode.cpp | 2 +- .../opencs/view/render/terraintexturemode.cpp | 2 +- apps/opencs/view/render/worldspacewidget.cpp | 27 ++++----- apps/opencs/view/render/worldspacewidget.hpp | 2 - apps/opencs/view/tools/reporttable.cpp | 2 +- apps/opencs/view/widget/colorpickerpopup.cpp | 2 +- apps/opencs/view/widget/completerpopup.cpp | 6 +- apps/opencs/view/world/dialoguesubview.cpp | 2 +- apps/opencs/view/world/dragrecordtable.cpp | 4 +- apps/opencs/view/world/regionmap.cpp | 4 +- apps/opencs/view/world/scriptedit.cpp | 6 +- apps/opencs/view/world/table.cpp | 4 +- .../world/tableheadermouseeventhandler.cpp | 2 +- apps/opencs/view/world/util.cpp | 6 +- components/config/gamesettings.cpp | 5 -- components/l10n/qttranslations.cpp | 4 -- components/misc/utf8qtextstream.hpp | 7 --- extern/osgQt/CMakeLists.txt | 6 +- 36 files changed, 112 insertions(+), 187 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 48d9f5ff8a..94e8866a40 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,14 +27,14 @@ variables: .Ubuntu_Image: tags: - saas-linux-medium-amd64 - image: ubuntu:22.04 + image: ubuntu:24.04 rules: - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" Ubuntu_GCC_preprocess: extends: .Ubuntu_Image cache: - key: Ubuntu_GCC_preprocess.ubuntu_22.04.v1 + key: Ubuntu_GCC_preprocess.ubuntu_24.04.v1 paths: - apt-cache/ - .cache/pip/ @@ -43,7 +43,7 @@ Ubuntu_GCC_preprocess: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" before_script: - CI/install_debian_deps.sh openmw-deps openmw-deps-dynamic gcc_preprocess - - pip3 install --user click termtables + - pip3 install --user --break-system-packages click termtables script: - CI/ubuntu_gcc_preprocess.sh rules: @@ -97,12 +97,12 @@ Ubuntu_GCC_preprocess: Coverity: tags: - saas-linux-medium-amd64 - image: ubuntu:22.04 + image: ubuntu:24.04 stage: build rules: - if: $CI_PIPELINE_SOURCE == "schedule" cache: - key: Coverity.ubuntu_22.04.v1 + key: Coverity.ubuntu_24.04.v1 paths: - apt-cache/ - ccache/ @@ -141,7 +141,7 @@ Coverity: Ubuntu_GCC: extends: .Ubuntu cache: - key: Ubuntu_GCC.ubuntu_22.04.v1 + key: Ubuntu_GCC.ubuntu_24.04.v1 before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic variables: @@ -154,7 +154,7 @@ Ubuntu_GCC: Ubuntu_GCC_asan: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_asan.ubuntu_22.04.v1 + key: Ubuntu_GCC_asan.ubuntu_24.04.v1 variables: CMAKE_BUILD_TYPE: Debug CMAKE_CXX_FLAGS_DEBUG: -g -O1 -fno-omit-frame-pointer -fsanitize=address -fsanitize=pointer-subtract -fsanitize=leak @@ -167,7 +167,7 @@ Clang_Format: extends: .Ubuntu_Image stage: checks cache: - key: Ubuntu_Clang_Format.ubuntu_22.04.v1 + key: Ubuntu_Clang_Format.ubuntu_24.04.v1 paths: - apt-cache/ variables: @@ -183,11 +183,11 @@ Lupdate: extends: .Ubuntu_Image stage: checks cache: - key: Ubuntu_lupdate.ubuntu_22.04.v1 + key: Ubuntu_lupdate.ubuntu_24.04.v1 paths: - apt-cache/ variables: - LUPDATE: lupdate + LUPDATE: /usr/lib/qt6/bin/lupdate before_script: - CI/install_debian_deps.sh openmw-qt-translations script: @@ -209,7 +209,7 @@ Teal: Ubuntu_GCC_Debug: extends: .Ubuntu cache: - key: Ubuntu_GCC_Debug.ubuntu_22.04.v2 + key: Ubuntu_GCC_Debug.ubuntu_24.04.v2 before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic variables: @@ -225,7 +225,7 @@ Ubuntu_GCC_Debug: Ubuntu_GCC_tests: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -239,7 +239,7 @@ Ubuntu_GCC_tests: .Ubuntu_GCC_tests_Debug: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests_Debug.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_Debug.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -255,7 +255,7 @@ Ubuntu_GCC_tests: Ubuntu_GCC_tests_asan: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests_asan.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_asan.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -278,7 +278,7 @@ Ubuntu_GCC_tests_asan: Ubuntu_GCC_tests_ubsan: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests_ubsan.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_ubsan.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -298,7 +298,7 @@ Ubuntu_GCC_tests_ubsan: .Ubuntu_GCC_tests_tsan: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests_tsan.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_tsan.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -316,7 +316,7 @@ Ubuntu_GCC_tests_ubsan: Ubuntu_GCC_tests_coverage: extends: .Ubuntu_GCC_tests_Debug cache: - key: Ubuntu_GCC_tests_coverage.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_coverage.ubuntu_24.04.v1 variables: BUILD_WITH_CODE_COVERAGE: 1 before_script: @@ -345,7 +345,7 @@ Ubuntu_GCC_tests_coverage: - "CI/**/*" - ".gitlab-ci.yml" cache: - key: Ubuntu_Static_Deps.ubuntu_22.04.v1 + key: Ubuntu_Static_Deps.ubuntu_24.04.v1 paths: - apt-cache/ - ccache/ @@ -362,7 +362,7 @@ Ubuntu_GCC_tests_coverage: .Ubuntu_Static_Deps_tests: extends: .Ubuntu_Static_Deps cache: - key: Ubuntu_Static_Deps_tests.ubuntu_22.04.v1 + key: Ubuntu_Static_Deps_tests.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -381,7 +381,7 @@ Ubuntu_Clang: before_script: - CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic cache: - key: Ubuntu_Clang.ubuntu_22.04.v2 + key: Ubuntu_Clang.ubuntu_24.04.v2 variables: CC: clang CXX: clang++ @@ -394,7 +394,7 @@ Ubuntu_Clang: before_script: - CI/install_debian_deps.sh clang clang-tidy openmw-deps openmw-deps-dynamic cache: - key: Ubuntu_Clang_Tidy.ubuntu_22.04.v1 + key: Ubuntu_Clang_Tidy.ubuntu_24.04.v1 variables: CMAKE_BUILD_TYPE: Debug CMAKE_CXX_FLAGS_DEBUG: -O0 @@ -451,7 +451,7 @@ Ubuntu_Clang_Tidy_other: .Ubuntu_Clang_tests: extends: Ubuntu_Clang cache: - key: Ubuntu_Clang_tests.ubuntu_22.04.v1 + key: Ubuntu_Clang_tests.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -465,7 +465,7 @@ Ubuntu_Clang_Tidy_other: Ubuntu_Clang_tests_Debug: extends: Ubuntu_Clang cache: - key: Ubuntu_Clang_tests_Debug.ubuntu_22.04.v1 + key: Ubuntu_Clang_tests_Debug.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -489,7 +489,7 @@ Ubuntu_Clang_tests_Debug: - apt-cache/ before_script: - CI/install_debian_deps.sh $OPENMW_DEPS - - pip3 install --user numpy matplotlib termtables click + - pip3 install --user --break-system-packages numpy matplotlib termtables click script: - CI/run_integration_tests.sh after_script: @@ -500,7 +500,7 @@ Ubuntu_Clang_integration_tests: needs: - Ubuntu_Clang cache: - key: Ubuntu_Clang_integration_tests.ubuntu_22.04.v2 + key: Ubuntu_Clang_integration_tests.ubuntu_24.04.v2 variables: OPENMW_DEPS: openmw-integration-tests @@ -509,9 +509,9 @@ Ubuntu_GCC_integration_tests_asan: needs: - Ubuntu_GCC_asan cache: - key: Ubuntu_GCC_integration_tests_asan.ubuntu_22.04.v1 + key: Ubuntu_GCC_integration_tests_asan.ubuntu_24.04.v1 variables: - OPENMW_DEPS: openmw-integration-tests libasan6 + OPENMW_DEPS: openmw-integration-tests libasan ASAN_OPTIONS: halt_on_error=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_leaks=0 .MacOS: @@ -990,7 +990,7 @@ Windows_MSBuild_CacheInit: paths: - .cache/pip before_script: - - pip3 install --user requests click discord_webhook + - pip3 install --user --break-system-packages requests click discord_webhook script: - scripts/find_missing_merge_requests.py --project_id=$CI_PROJECT_ID --ignored_mrs_path=$CI_PROJECT_DIR/.resubmitted_merge_requests.txt diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 9363211e9a..4f62ef03d9 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -708,17 +708,12 @@ printf "Qt ${QT_VER}... " DLLSUFFIX="" fi - if [ "${QT_MAJOR_VER}" -eq 6 ]; then - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets,Svg}${DLLSUFFIX}.dll + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets,Svg}${DLLSUFFIX}.dll - # Since Qt 6.7.0 plugin is called "qmodernwindowsstyle" - if [ "${QT_MINOR_VER}" -ge 7 ]; then - add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qmodernwindowsstyle${DLLSUFFIX}.dll" - else - add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" - fi + # Since Qt 6.7.0 plugin is called "qmodernwindowsstyle" + if [ "${QT_MINOR_VER}" -ge 7 ]; then + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qmodernwindowsstyle${DLLSUFFIX}.dll" else - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,Widgets,Svg}${DLLSUFFIX}.dll add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" fi diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh index 3ba66133ca..79e759b871 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -33,10 +33,10 @@ declare -rA GROUPED_DEPS=( libboost-system-dev libboost-iostreams-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev - libsdl2-dev libqt5opengl5-dev qttools5-dev qttools5-dev-tools libopenal-dev + libsdl2-dev libqt6opengl6-dev qt6-tools-dev qt6-tools-dev-tools libopenal-dev libunshield-dev libtinyxml-dev libbullet-dev liblz4-dev libpng-dev libjpeg-dev libluajit-5.1-dev librecast-dev libsqlite3-dev ca-certificates libicu-dev - libyaml-cpp-dev libqt5svg5 libqt5svg5-dev + libyaml-cpp-dev libqt6svg6 libqt6svg6-dev " # These dependencies can alternatively be built and linked statically. @@ -64,15 +64,15 @@ declare -rA GROUPED_DEPS=( gdb git git-lfs - libavcodec58 - libavformat58 - libavutil56 - libboost-iostreams1.74.0 - libboost-program-options1.74.0 - libboost-system1.74.0 + libavcodec60 + libavformat60 + libavutil58 + libboost-iostreams1.83.0 + libboost-program-options1.83.0 + libboost-system1.83.0 libbullet3.24 libcollada-dom2.5-dp0 - libicu70 + libicu74 libjpeg8 libluajit-5.1-2 liblz4-1 @@ -80,19 +80,19 @@ declare -rA GROUPED_DEPS=( libopenal1 libopenscenegraph161 libpng16-16 - libqt5opengl5 + libqt6opengl6 librecast1 libsdl2-2.0-0 libsqlite3-0 - libswresample3 - libswscale5 + libswresample4 + libswscale7 libtinyxml2.6.2v5 libyaml-cpp0.8 python3-pip xvfb " - [libasan6]="libasan6" + [libasan]="libasan8" [android]="binutils build-essential cmake ccache curl unzip git pkg-config" @@ -102,8 +102,8 @@ declare -rA GROUPED_DEPS=( " [openmw-qt-translations]=" - qttools5-dev - qttools5-dev-tools + qt6-tools-dev + qt6-tools-dev-tools git-core " ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 78d9aae646..c062949ec4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -249,12 +249,8 @@ endif() find_package(LZ4 REQUIRED) if (USE_QT) - find_package(QT REQUIRED COMPONENTS Core NAMES Qt6 Qt5) - if (QT_VERSION_MAJOR VERSION_EQUAL 5) - find_package(Qt5 5.15 COMPONENTS Core Widgets Network OpenGL LinguistTools Svg REQUIRED) - else() - find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets LinguistTools Svg REQUIRED) - endif() + find_package(QT REQUIRED COMPONENTS Core NAMES Qt6) + find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets LinguistTools Svg REQUIRED) message(STATUS "Using Qt${QT_VERSION}") endif() diff --git a/apps/launcher/importpage.cpp b/apps/launcher/importpage.cpp index 3ad6e538da..461ba4c2eb 100644 --- a/apps/launcher/importpage.cpp +++ b/apps/launcher/importpage.cpp @@ -88,11 +88,7 @@ void Launcher::ImportPage::on_importerButton_clicked() // Create the file if it doesn't already exist, else the importer will fail auto path = mCfgMgr.getUserConfigPath(); path /= "openmw.cfg"; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QFile file(path); -#else - QFile file(Files::pathToQString(path)); -#endif if (!file.exists()) { diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 07face085f..2aa80d346c 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -497,11 +497,7 @@ bool Launcher::MainDialog::writeSettings() } // Game settings -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QFile file(userPath / Files::openmwCfgFile); -#else - QFile file(Files::getUserConfigPathQString(mCfgMgr)); -#endif if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 0a278316e3..c9ca7838be 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -240,11 +240,7 @@ target_link_libraries(openmw-cs-lib components_qt ) -if (QT_VERSION_MAJOR VERSION_EQUAL 6) - target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::Svg) -else() - target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::Svg) -endif() +target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::Svg) if (WIN32) target_sources(openmw-cs PRIVATE ${CMAKE_SOURCE_DIR}/files/windows/openmw-cs.exe.manifest) diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp index 7d837f9e54..e8b89c5277 100644 --- a/apps/opencs/model/filter/textnode.cpp +++ b/apps/opencs/model/filter/textnode.cpp @@ -34,11 +34,11 @@ bool CSMFilter::TextNode::test(const CSMWorld::IdTableBase& table, int row, cons QString string; - if (data.type() == QVariant::String) + if (data.typeId() == QMetaType::QString) { string = data.toString(); } - else if ((data.type() == QVariant::Int || data.type() == QVariant::UInt) + else if ((data.typeId() == QMetaType::Int || data.typeId() == QMetaType::UInt) && CSMWorld::Columns::hasEnums(static_cast(mColumnId))) { int value = data.toInt(); @@ -49,7 +49,7 @@ bool CSMFilter::TextNode::test(const CSMWorld::IdTableBase& table, int row, cons if (value >= 0 && value < static_cast(enums.size())) string = QString::fromUtf8(enums[value].second.c_str()); } - else if (data.type() == QVariant::Bool) + else if (data.typeId() == QMetaType::Bool) { string = data.toBool() ? "true" : "false"; } diff --git a/apps/opencs/model/filter/valuenode.cpp b/apps/opencs/model/filter/valuenode.cpp index 264967f240..abbf00aecd 100644 --- a/apps/opencs/model/filter/valuenode.cpp +++ b/apps/opencs/model/filter/valuenode.cpp @@ -29,8 +29,8 @@ bool CSMFilter::ValueNode::test(const CSMWorld::IdTableBase& table, int row, con QVariant data = table.data(index); - if (data.type() != QVariant::Double && data.type() != QVariant::Bool && data.type() != QVariant::Int - && data.type() != QVariant::UInt && data.type() != static_cast(QMetaType::Float)) + if (data.typeId() != QMetaType::Double && data.typeId() != QMetaType::Bool && data.typeId() != QMetaType::Int + && data.typeId() != QMetaType::UInt && data.typeId() != QMetaType::Float) return false; double value = data.toDouble(); diff --git a/apps/opencs/model/prefs/shortcuteventhandler.cpp b/apps/opencs/model/prefs/shortcuteventhandler.cpp index 4bbda2996f..a7e9dfbb3a 100644 --- a/apps/opencs/model/prefs/shortcuteventhandler.cpp +++ b/apps/opencs/model/prefs/shortcuteventhandler.cpp @@ -62,39 +62,31 @@ namespace CSMPrefs { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); - unsigned int mod = (unsigned int)keyEvent->modifiers(); - unsigned int key = (unsigned int)keyEvent->key(); if (!keyEvent->isAutoRepeat()) - return activate(widget, mod, key); + return activate(widget, keyEvent->keyCombination()); } else if (event->type() == QEvent::KeyRelease) { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); - unsigned int mod = (unsigned int)keyEvent->modifiers(); - unsigned int key = (unsigned int)keyEvent->key(); if (!keyEvent->isAutoRepeat()) - return deactivate(widget, mod, key); + return deactivate(widget, keyEvent->keyCombination()); } else if (event->type() == QEvent::MouseButtonPress) { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); - unsigned int mod = (unsigned int)mouseEvent->modifiers(); - unsigned int button = (unsigned int)mouseEvent->button(); - return activate(widget, mod, button); + return activate(widget, QKeyCombination(mouseEvent->modifiers(), Qt::Key(mouseEvent->button()))); } else if (event->type() == QEvent::MouseButtonRelease) { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); - unsigned int mod = (unsigned int)mouseEvent->modifiers(); - unsigned int button = (unsigned int)mouseEvent->button(); - return deactivate(widget, mod, button); + return deactivate(widget, QKeyCombination(mouseEvent->modifiers(), Qt::Key(mouseEvent->button()))); } else if (event->type() == QEvent::FocusOut) { @@ -149,7 +141,7 @@ namespace CSMPrefs } } - bool ShortcutEventHandler::activate(QWidget* widget, unsigned int mod, unsigned int button) + bool ShortcutEventHandler::activate(QWidget* widget, QKeyCombination keyCombination) { std::vector> potentials; bool used = false; @@ -167,7 +159,7 @@ namespace CSMPrefs if (!shortcut->isEnabled()) continue; - if (checkModifier(mod, button, shortcut, true)) + if (checkModifier(keyCombination, shortcut, true)) used = true; if (shortcut->getActivationStatus() != Shortcut::AS_Inactive) @@ -175,7 +167,8 @@ namespace CSMPrefs int pos = shortcut->getPosition(); int lastPos = shortcut->getLastPosition(); - MatchResult result = match(mod, button, shortcut->getSequence()[pos]); + MatchResult result = match(keyCombination.keyboardModifiers(), keyCombination.key(), + shortcut->getSequence()[pos].toCombined()); if (result == Matches_WithMod || result == Matches_NoMod) { @@ -220,10 +213,8 @@ namespace CSMPrefs return used; } - bool ShortcutEventHandler::deactivate(QWidget* widget, unsigned int mod, unsigned int button) + bool ShortcutEventHandler::deactivate(QWidget* widget, QKeyCombination keyCombination) { - const int KeyMask = 0x01FFFFFF; - bool used = false; while (widget) @@ -235,11 +226,11 @@ namespace CSMPrefs { Shortcut* shortcut = *it; - if (checkModifier(mod, button, shortcut, false)) + if (checkModifier(keyCombination, shortcut, false)) used = true; int pos = shortcut->getPosition(); - MatchResult result = match(0, button, shortcut->getSequence()[pos] & KeyMask); + MatchResult result = match(0, keyCombination.key(), shortcut->getSequence()[pos].key()); if (result != Matches_Not) { @@ -268,13 +259,13 @@ namespace CSMPrefs return used; } - bool ShortcutEventHandler::checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate) + bool ShortcutEventHandler::checkModifier(QKeyCombination keyCombination, Shortcut* shortcut, bool activate) { if (!shortcut->isEnabled() || !shortcut->getModifier() || shortcut->getSecondaryMode() == Shortcut::SM_Ignore || shortcut->getModifierStatus() == activate) return false; - MatchResult result = match(mod, button, shortcut->getModifier()); + MatchResult result = match(keyCombination.keyboardModifiers(), keyCombination.key(), shortcut->getModifier()); bool used = false; if (result != Matches_Not) diff --git a/apps/opencs/model/prefs/shortcuteventhandler.hpp b/apps/opencs/model/prefs/shortcuteventhandler.hpp index 2093e259e9..cc94797450 100644 --- a/apps/opencs/model/prefs/shortcuteventhandler.hpp +++ b/apps/opencs/model/prefs/shortcuteventhandler.hpp @@ -42,11 +42,11 @@ namespace CSMPrefs void updateParent(QWidget* widget); - bool activate(QWidget* widget, unsigned int mod, unsigned int button); + bool activate(QWidget* widget, QKeyCombination keyCombination); - bool deactivate(QWidget* widget, unsigned int mod, unsigned int button); + bool deactivate(QWidget* widget, QKeyCombination keyCombination); - bool checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate); + bool checkModifier(QKeyCombination keyCombination, Shortcut* shortcut, bool activate); MatchResult match(unsigned int mod, unsigned int button, unsigned int value); diff --git a/apps/opencs/model/prefs/shortcutmanager.cpp b/apps/opencs/model/prefs/shortcutmanager.cpp index ac032efffb..2ffa042641 100644 --- a/apps/opencs/model/prefs/shortcutmanager.cpp +++ b/apps/opencs/model/prefs/shortcutmanager.cpp @@ -115,15 +115,12 @@ namespace CSMPrefs std::string ShortcutManager::convertToString(const QKeySequence& sequence) const { - const int MouseKeyMask = 0x01FFFFFF; - const int ModMask = 0x7E000000; - std::string result; - for (int i = 0; i < (int)sequence.count(); ++i) + for (int i = 0; i < sequence.count(); ++i) { - int mods = sequence[i] & ModMask; - int key = sequence[i] & MouseKeyMask; + int mods = sequence[i].keyboardModifiers(); + int key = sequence[i].key(); if (key) { diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index b80d150448..4d191b90a4 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -59,13 +59,6 @@ void CSMPrefs::State::declare() .setTooltip("Minimum width of subviews.") .setRange(50, 10000); declareEnum(mValues->mWindows.mMainwindowScrollbar, "Main Window Horizontal Scrollbar Mode"); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - declareBool(mValues->mWindows.mGrowLimit, "Grow Limit Screen") - .setTooltip( - "When \"Grow then Scroll\" option is selected, the window size grows to" - " the width of the virtual desktop. \nIf this option is selected the the window growth" - "is limited to the current screen."); -#endif declareCategory("Records"); declareEnum(mValues->mRecords.mStatusFormat, "Modification Status Display Format"); diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index afcab50ead..ffc7400da3 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -659,11 +659,7 @@ void CSVDoc::View::addSubView(const CSMWorld::UniversalId& id, const std::string // mScrollbarOnly = windows["mainwindow-scrollbar"].toString() == "Scrollbar Only"; -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - updateWidth(windows["grow-limit"].isTrue(), minWidth); -#else updateWidth(true, minWidth); -#endif mSubViewWindow.addDockWidget(Qt::TopDockWidgetArea, view); diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp index f2ecd5f642..7efbd8e7a3 100644 --- a/apps/opencs/view/filter/editwidget.hpp +++ b/apps/opencs/view/filter/editwidget.hpp @@ -50,7 +50,7 @@ namespace CSVFilter std::pair operator()(const QVariant& variantData) { FilterType filterType = FilterType::String; - QMetaType::Type dataType = static_cast(variantData.type()); + QMetaType::Type dataType = static_cast(variantData.typeId()); if (dataType == QMetaType::QString || dataType == QMetaType::Bool || dataType == QMetaType::Int) filterType = FilterType::String; if (dataType == QMetaType::Int || dataType == QMetaType::Float) diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index 76cfd57ba6..e100a69a7c 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -1132,7 +1132,7 @@ void CSVRender::InstanceMode::dropEvent(QDropEvent* event) return; WorldspaceHitResult hit - = getWorldspaceWidget().mousePick(event->pos(), getWorldspaceWidget().getInteractionMask()); + = getWorldspaceWidget().mousePick(event->position().toPoint(), getWorldspaceWidget().getInteractionMask()); std::string cellId = getWorldspaceWidget().getCellId(hit.worldPos); diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index e84433f14c..5e8db03cf8 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -161,8 +161,6 @@ namespace CSVRender , mLighting(nullptr) , mHasDefaultAmbient(false) , mIsExterior(true) - , mPrevMouseX(0) - , mPrevMouseY(0) , mCamPositionSet(false) { mFreeCamControl = new FreeCameraController(this); @@ -423,10 +421,10 @@ namespace CSVRender void SceneWidget::mouseMoveEvent(QMouseEvent* event) { - mCurrentCamControl->handleMouseMoveEvent(event->x() - mPrevMouseX, event->y() - mPrevMouseY); + QPointF pos = event->position(); + mCurrentCamControl->handleMouseMoveEvent(pos.x() - mPrevMouse.x(), pos.y() - mPrevMouse.y()); - mPrevMouseX = event->x(); - mPrevMouseY = event->y(); + mPrevMouse = pos; } void SceneWidget::wheelEvent(QWheelEvent* event) diff --git a/apps/opencs/view/render/scenewidget.hpp b/apps/opencs/view/render/scenewidget.hpp index aeec5d191c..2e9526eff6 100644 --- a/apps/opencs/view/render/scenewidget.hpp +++ b/apps/opencs/view/render/scenewidget.hpp @@ -137,7 +137,7 @@ namespace CSVRender LightingNight mLightingNight; LightingBright mLightingBright; - int mPrevMouseX, mPrevMouseY; + QPointF mPrevMouse; /// Tells update that camera isn't set bool mCamPositionSet; diff --git a/apps/opencs/view/render/terrainshapemode.cpp b/apps/opencs/view/render/terrainshapemode.cpp index bc1c6c6365..4f8bd44f05 100644 --- a/apps/opencs/view/render/terrainshapemode.cpp +++ b/apps/opencs/view/render/terrainshapemode.cpp @@ -1661,7 +1661,7 @@ void CSVRender::TerrainShapeMode::dragMoveEvent(QDragMoveEvent* event) {} void CSVRender::TerrainShapeMode::mouseMoveEvent(QMouseEvent* event) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->position().toPoint(), getInteractionMask()); if (hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); if (!hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index cfc7f50cf1..b3fd64705d 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -724,7 +724,7 @@ void CSVRender::TerrainTextureMode::dragMoveEvent(QDragMoveEvent* event) {} void CSVRender::TerrainTextureMode::mouseMoveEvent(QMouseEvent* event) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->position().toPoint(), getInteractionMask()); if (hit.hit && mBrushDraw) mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); if (!hit.hit && mBrushDraw) diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 915282ab45..6254017ec2 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -687,11 +687,12 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event) if (mDragging) { - int diffX = event->x() - mDragX; - int diffY = (height() - event->y()) - mDragY; + QPoint pos = event->position().toPoint(); + int diffX = pos.x() - mDragX; + int diffY = (height() - pos.y()) - mDragY; - mDragX = event->x(); - mDragY = height() - event->y(); + mDragX = pos.x(); + mDragY = height() - pos.y(); double factor = mDragFactor; @@ -700,32 +701,32 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event) EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); - editMode.drag(event->pos(), diffX, diffY, factor); + editMode.drag(event->position().toPoint(), diffX, diffY, factor); } else if (mDragMode != InteractionType_None) { EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); if (mDragMode == InteractionType_PrimaryEdit) - mDragging = editMode.primaryEditStartDrag(event->pos()); + mDragging = editMode.primaryEditStartDrag(event->position().toPoint()); else if (mDragMode == InteractionType_SecondaryEdit) - mDragging = editMode.secondaryEditStartDrag(event->pos()); + mDragging = editMode.secondaryEditStartDrag(event->position().toPoint()); else if (mDragMode == InteractionType_PrimarySelect) - mDragging = editMode.primarySelectStartDrag(event->pos()); + mDragging = editMode.primarySelectStartDrag(event->position().toPoint()); else if (mDragMode == InteractionType_SecondarySelect) - mDragging = editMode.secondarySelectStartDrag(event->pos()); + mDragging = editMode.secondarySelectStartDrag(event->position().toPoint()); if (mDragging) { - mDragX = event->localPos().x(); - mDragY = height() - event->localPos().y(); + mDragX = event->position().x(); + mDragY = height() - event->position().y(); } } else { - if (event->globalPos() != mToolTipPos) + if (event->globalPosition().toPoint() != mToolTipPos) { - mToolTipPos = event->globalPos(); + mToolTipPos = event->globalPosition().toPoint(); if (mShowToolTips) { diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index a223a54388..831ed13640 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -260,8 +260,6 @@ namespace CSVRender void settingChanged(const CSMPrefs::Setting* setting) override; - bool getSpeedMode(); - void cycleNavigationMode(); private: diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp index 7ec55b96b5..8c92d9980f 100644 --- a/apps/opencs/view/tools/reporttable.cpp +++ b/apps/opencs/view/tools/reporttable.cpp @@ -93,7 +93,7 @@ void CSVTools::ReportTable::contextMenuEvent(QContextMenuEvent* event) void CSVTools::ReportTable::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) - startDragFromTable(*this, indexAt(event->pos())); + startDragFromTable(*this, indexAt(event->position().toPoint())); } void CSVTools::ReportTable::mouseDoubleClickEvent(QMouseEvent* event) diff --git a/apps/opencs/view/widget/colorpickerpopup.cpp b/apps/opencs/view/widget/colorpickerpopup.cpp index 87c62e137b..0918e798a5 100644 --- a/apps/opencs/view/widget/colorpickerpopup.cpp +++ b/apps/opencs/view/widget/colorpickerpopup.cpp @@ -52,7 +52,7 @@ void CSVWidget::ColorPickerPopup::mousePressEvent(QMouseEvent* event) // If the mouse is pressed above the pop-up parent, // the pop-up will be hidden and the pressed signal won't be repeated for the parent - if (buttonRect.contains(event->globalPos()) || buttonRect.contains(event->pos())) + if (buttonRect.contains(event->globalPosition().toPoint()) || buttonRect.contains(event->position().toPoint())) { setAttribute(Qt::WA_NoMouseReplay); } diff --git a/apps/opencs/view/widget/completerpopup.cpp b/apps/opencs/view/widget/completerpopup.cpp index 91b0902659..25daee1f1f 100644 --- a/apps/opencs/view/widget/completerpopup.cpp +++ b/apps/opencs/view/widget/completerpopup.cpp @@ -22,12 +22,8 @@ int CSVWidget::CompleterPopup::sizeHintForRow(int row) const ensurePolished(); QModelIndex index = model()->index(row, modelColumn()); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif - QAbstractItemDelegate* delegate = itemDelegate(index); + QAbstractItemDelegate* delegate = itemDelegateForIndex(index); return delegate->sizeHint(option, index).height(); } diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index 168e555eae..ac69805134 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -85,7 +85,7 @@ void CSVWorld::NotEditableSubDelegate::setEditorData(QWidget* editor, const QMod CSMWorld::Columns::ColumnId columnId = static_cast(mTable->getColumnId(index.column())); - if (QVariant::String == v.type()) + if (QMetaType::QString == v.typeId()) { label->setText(v.toString()); } diff --git a/apps/opencs/view/world/dragrecordtable.cpp b/apps/opencs/view/world/dragrecordtable.cpp index a006779ba4..adff809335 100644 --- a/apps/opencs/view/world/dragrecordtable.cpp +++ b/apps/opencs/view/world/dragrecordtable.cpp @@ -55,7 +55,7 @@ void CSVWorld::DragRecordTable::dragEnterEvent(QDragEnterEvent* event) void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent* event) { - QModelIndex index = indexAt(event->pos()); + QModelIndex index = indexAt(event->position().toPoint()); if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)) || CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index)) || CSVWorld::DragDropUtils::isTopicOrJournal(*event, getIndexDisplayType(index))) @@ -71,7 +71,7 @@ void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent* event) void CSVWorld::DragRecordTable::dropEvent(QDropEvent* event) { - QModelIndex index = indexAt(event->pos()); + QModelIndex index = indexAt(event->position().toPoint()); CSMWorld::ColumnBase::Display display = getIndexDisplayType(index); if (CSVWorld::DragDropUtils::canAcceptData(*event, display)) { diff --git a/apps/opencs/view/world/regionmap.cpp b/apps/opencs/view/world/regionmap.cpp index 17d0016afc..ae9ac94022 100644 --- a/apps/opencs/view/world/regionmap.cpp +++ b/apps/opencs/view/world/regionmap.cpp @@ -341,7 +341,7 @@ void CSVWorld::RegionMap::viewInTable() void CSVWorld::RegionMap::mouseMoveEvent(QMouseEvent* event) { - startDragFromTable(*this, indexAt(event->pos())); + startDragFromTable(*this, indexAt(event->position().toPoint())); } std::vector CSVWorld::RegionMap::getDraggedRecords() const @@ -376,7 +376,7 @@ void CSVWorld::RegionMap::dragMoveEvent(QDragMoveEvent* event) void CSVWorld::RegionMap::dropEvent(QDropEvent* event) { - QModelIndex index = indexAt(event->pos()); + QModelIndex index = indexAt(event->position().toPoint()); bool exists = QTableView::model()->data(index, Qt::BackgroundRole) != QBrush(Qt::DiagCrossPattern); if (!index.isValid() || !exists) diff --git a/apps/opencs/view/world/scriptedit.cpp b/apps/opencs/view/world/scriptedit.cpp index 00acf71235..2871e0ced1 100644 --- a/apps/opencs/view/world/scriptedit.cpp +++ b/apps/opencs/view/world/scriptedit.cpp @@ -136,7 +136,7 @@ void CSVWorld::ScriptEdit::dragEnterEvent(QDragEnterEvent* event) QPlainTextEdit::dragEnterEvent(event); else { - setTextCursor(cursorForPosition(event->pos())); + setTextCursor(cursorForPosition(event->position().toPoint())); event->acceptProposedAction(); } } @@ -148,7 +148,7 @@ void CSVWorld::ScriptEdit::dragMoveEvent(QDragMoveEvent* event) QPlainTextEdit::dragMoveEvent(event); else { - setTextCursor(cursorForPosition(event->pos())); + setTextCursor(cursorForPosition(event->position().toPoint())); event->accept(); } } @@ -162,7 +162,7 @@ void CSVWorld::ScriptEdit::dropEvent(QDropEvent* event) return; } - setTextCursor(cursorForPosition(event->pos())); + setTextCursor(cursorForPosition(event->position().toPoint())); if (mime->fromDocument(mDocument)) { diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 86a1e93bbd..2cadbf289a 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -592,7 +592,7 @@ void CSVWorld::Table::moveRecords(QDropEvent* event) if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; - QModelIndex targedIndex = indexAt(event->pos()); + QModelIndex targedIndex = indexAt(event->position().toPoint()); QModelIndexList selectedRows = selectionModel()->selectedRows(); int targetRowRaw = targedIndex.row(); @@ -872,7 +872,7 @@ void CSVWorld::Table::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) { - startDragFromTable(*this, indexAt(event->pos())); + startDragFromTable(*this, indexAt(event->position().toPoint())); } } diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.cpp b/apps/opencs/view/world/tableheadermouseeventhandler.cpp index dcd2e659a6..2bc6c4fe00 100644 --- a/apps/opencs/view/world/tableheadermouseeventhandler.cpp +++ b/apps/opencs/view/world/tableheadermouseeventhandler.cpp @@ -31,7 +31,7 @@ namespace CSVWorld auto& clickEvent = static_cast(*event); if ((clickEvent.button() == Qt::MiddleButton)) { - const auto& index = table.indexAt(clickEvent.pos()); + const auto& index = table.indexAt(clickEvent.position().toPoint()); table.setColumnHidden(index.column(), true); clickEvent.accept(); return true; diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index dfb587cd96..39b8ba321e 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -171,7 +171,7 @@ QWidget* CSVWorld::CommandDelegate::createEditor( // TODO: Find a better solution? if (display == CSMWorld::ColumnBase::Display_Boolean) { - return QItemEditorFactory::defaultFactory()->createEditor(QVariant::Bool, parent); + return QItemEditorFactory::defaultFactory()->createEditor(QMetaType::Bool, parent); } // For tables the pop-up of the color editor should appear immediately after the editor creation // (the third parameter of ColorEditor's constructor) @@ -362,11 +362,7 @@ void CSVWorld::CommandDelegate::setEditorData(QWidget* editor, const QModelIndex if (!n.isEmpty()) { if (!variant.isValid()) -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) variant = QVariant(editor->property(n).metaType(), (const void*)nullptr); -#else - variant = QVariant(editor->property(n).userType(), (const void*)nullptr); -#endif editor->setProperty(n, variant); } } diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 7f022bb653..f5d9918c10 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -25,11 +25,6 @@ namespace Config::GameSettings::GameSettings(const Files::ConfigurationManager& cfg) : mCfgMgr(cfg) { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - // this needs calling once so Qt can see its stream operators, which it needs when dragging and dropping - // it's automatic with Qt 6 - qRegisterMetaTypeStreamOperators("Config::SettingValue"); -#endif } void Config::GameSettings::validatePaths() diff --git a/components/l10n/qttranslations.cpp b/components/l10n/qttranslations.cpp index 1051b3dd2d..fbc38dc122 100644 --- a/components/l10n/qttranslations.cpp +++ b/components/l10n/qttranslations.cpp @@ -14,11 +14,7 @@ namespace L10n // Try to load OpenMW translations from resources folder first. // If we loaded them, try to load Qt translations from both // resources folder and default translations folder as well. -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto qtPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath); -#else - auto qtPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath); -#endif auto localPath = resourcesPath + "/translations"; if (AppTranslator.load(QLocale::system(), appName, "_", localPath) diff --git a/components/misc/utf8qtextstream.hpp b/components/misc/utf8qtextstream.hpp index a1e101864f..1b8bd1d786 100644 --- a/components/misc/utf8qtextstream.hpp +++ b/components/misc/utf8qtextstream.hpp @@ -3,20 +3,13 @@ #include -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -#include -#endif #include namespace Misc { inline void ensureUtf8Encoding(QTextStream& stream) { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - stream.setCodec(QTextCodec::codecForName("UTF-8")); -#else stream.setEncoding(QStringConverter::Utf8); -#endif } } #endif diff --git a/extern/osgQt/CMakeLists.txt b/extern/osgQt/CMakeLists.txt index 781305ca47..1cfb8230a3 100644 --- a/extern/osgQt/CMakeLists.txt +++ b/extern/osgQt/CMakeLists.txt @@ -17,11 +17,7 @@ set(OSGQT_SOURCE_FILES add_library(${OSGQT_LIBRARY} STATIC ${OSGQT_SOURCE_FILES}) -if (QT_VERSION_MAJOR VERSION_EQUAL 6) - target_link_libraries(${OSGQT_LIBRARY} Qt::Core Qt::OpenGL Qt::OpenGLWidgets) -else() - target_link_libraries(${OSGQT_LIBRARY} Qt::Core Qt::OpenGL) -endif() +target_link_libraries(${OSGQT_LIBRARY} Qt::Core Qt::OpenGL Qt::OpenGLWidgets) link_directories(${CMAKE_CURRENT_BINARY_DIR}) From bdb3387bff4abe84f300ba2fb2a721f75eb01dda Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 22 Jul 2025 19:18:12 +0200 Subject: [PATCH 07/33] Install gcovr via pipx and disable Werror when building Benchmarks --- .gitlab-ci.yml | 4 ++++ CI/install_debian_deps.sh | 2 +- apps/opencs/view/render/worldspacewidget.cpp | 2 +- extern/CMakeLists.txt | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 94e8866a40..1bf66930e3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -317,10 +317,14 @@ Ubuntu_GCC_tests_coverage: extends: .Ubuntu_GCC_tests_Debug cache: key: Ubuntu_GCC_tests_coverage.ubuntu_24.04.v1 + paths: + - .cache/pip variables: BUILD_WITH_CODE_COVERAGE: 1 + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic openmw-coverage + - pipx install gcovr coverage: /^\s*lines:\s*\d+.\d+\%/ artifacts: paths: [] diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh index 79e759b871..4f0e7cdb69 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -57,7 +57,7 @@ declare -rA GROUPED_DEPS=( libsdl2-dev libboost-system-dev libboost-filesystem-dev libgl-dev " - [openmw-coverage]="gcovr" + [openmw-coverage]="pipx" [openmw-integration-tests]=" ca-certificates diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 6254017ec2..836f5ff0ea 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -735,7 +735,7 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event) } } - const QPointF& pos = event->localPos(); + QPoint pos = event->position().toPoint(); handleMarkerHighlight(pos.x(), pos.y()); SceneWidget::mouseMoveEvent(event); } diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index 74ba957813..ef39dc20e1 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -209,6 +209,7 @@ if (BUILD_BENCHMARKS AND NOT OPENMW_USE_SYSTEM_BENCHMARK) set(BENCHMARK_ENABLE_TESTING OFF) set(BENCHMARK_ENABLE_INSTALL OFF) set(BENCHMARK_ENABLE_GTEST_TESTS OFF) + set(BENCHMARK_ENABLE_WERROR OFF) include(FetchContent) FetchContent_Declare(benchmark From 7f6fe15b10892c838fc6bd8bddddf16b116ada8f Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 22 Jul 2025 20:31:14 +0200 Subject: [PATCH 08/33] Use local gcovr --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1bf66930e3..e7fad9e762 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -82,7 +82,7 @@ Ubuntu_GCC_preprocess: - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_settings_access_benchmark; fi - ccache -svv - df -h - - if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then gcovr --xml-pretty --exclude-unreachable-branches --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi + - if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then ~/.local/bin/gcovr --xml-pretty --exclude-unreachable-branches --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi - ls | grep -v -e '^extern$' -e '^install$' -e '^components-tests.xml$' -e '^openmw-tests.xml$' -e '^openmw-cs-tests.xml$' | xargs -I '{}' rm -rf './{}' - cd .. - df -h From de683109929edd608074624f4fac0d43a3d8762a Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 22 Jul 2025 21:32:32 +0200 Subject: [PATCH 09/33] Ignore time travel --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e7fad9e762..fedc707bd7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -82,7 +82,7 @@ Ubuntu_GCC_preprocess: - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_settings_access_benchmark; fi - ccache -svv - df -h - - if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then ~/.local/bin/gcovr --xml-pretty --exclude-unreachable-branches --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi + - if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then ~/.local/bin/gcovr --xml-pretty --exclude-unreachable-branches --gcov-ignore-parse-errors=negative_hits.warn_once_per_file --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi - ls | grep -v -e '^extern$' -e '^install$' -e '^components-tests.xml$' -e '^openmw-tests.xml$' -e '^openmw-cs-tests.xml$' | xargs -I '{}' rm -rf './{}' - cd .. - df -h From 50f5bc51c68ca6d7bda64fc93f1859df2d9ca836 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 14:51:41 +0200 Subject: [PATCH 10/33] Store allowed positions as osg::Vec3f There is no need to store pathgrid points. --- apps/openmw/mwmechanics/aiwander.cpp | 168 ++++++++++++------------ apps/openmw/mwmechanics/aiwander.hpp | 31 ++--- apps/openmw/mwmechanics/pathfinding.hpp | 13 -- components/misc/coordinateconverter.hpp | 12 +- 4 files changed, 108 insertions(+), 116 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 2bdeb27179..a2d304c2e0 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -109,24 +109,25 @@ namespace MWMechanics return std::vector(std::begin(idle), std::end(idle)); } - void trimAllowedNodes(const std::deque& path, std::vector& nodes) + void trimAllowedPositions(const std::deque& path, std::vector& allowedPositions) { // TODO: how to add these back in once the door opens? // Idea: keep a list of detected closed doors (see aicombat.cpp) // Every now and then check whether one of the doors is opened. (maybe // at the end of playing idle?) If the door is opened then re-calculate - // allowed nodes starting from the spawn point. + // allowed positions starting from the spawn point. std::vector points(path.begin(), path.end()); while (points.size() >= 2) { const osg::Vec3f point = points.back(); - for (std::size_t j = 0; j < nodes.size(); j++) + for (std::size_t j = 0; j < allowedPositions.size(); j++) { // FIXME: doesn't handle a door with the same X/Y // coordinates but with a different Z - if (std::abs(nodes[j].mX - point.x()) <= 0.5 && std::abs(nodes[j].mY - point.y()) <= 0.5) + if (std::abs(allowedPositions[j].x() - point.x()) <= 0.5 + && std::abs(allowedPositions[j].y() - point.y()) <= 0.5) { - nodes.erase(nodes.begin() + j); + allowedPositions.erase(allowedPositions.begin() + j); break; } } @@ -143,9 +144,9 @@ namespace MWMechanics , mCanWanderAlongPathGrid(true) , mIdleAnimation(0) , mBadIdles() - , mPopulateAvailableNodes(true) - , mAllowedNodes() - , mTrimCurrentNode(false) + , mPopulateAvailablePositions(true) + , mAllowedPositions() + , mTrimCurrentPosition(false) , mCheckIdlePositionTimer(0) , mStuckCount(0) { @@ -298,10 +299,10 @@ namespace MWMechanics mStoredInitialActorPosition = true; } - // Initialization to discover & store allowed node points for this actor. - if (storage.mPopulateAvailableNodes) + // Initialization to discover & store allowed positions points for this actor. + if (storage.mPopulateAvailablePositions) { - getAllowedNodes(actor, storage); + fillAllowedPositions(actor, storage); } MWBase::World& world = *MWBase::Environment::get().getWorld(); @@ -319,7 +320,7 @@ namespace MWMechanics } // If the package has a wander distance but no pathgrid is available, // randomly idle or wander near spawn point - else if (storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) + else if (storage.mAllowedPositions.empty() && mDistance > 0 && !storage.mIsWanderingManually) { // Typically want to idle for a short time before the next wander if (Misc::Rng::rollDice(100, prng) >= 96) @@ -331,7 +332,7 @@ namespace MWMechanics storage.setState(AiWanderStorage::Wander_IdleNow); } } - else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) + else if (storage.mAllowedPositions.empty() && !storage.mIsWanderingManually) { storage.mCanWanderAlongPathGrid = false; } @@ -347,9 +348,9 @@ namespace MWMechanics // Construct a new path if there isn't one if (!mPathFinder.isPathConstructed()) { - if (!storage.mAllowedNodes.empty()) + if (!storage.mAllowedPositions.empty()) { - setPathToAnAllowedNode(actor, storage, pos); + setPathToAnAllowedPosition(actor, storage, pos); } } } @@ -515,17 +516,17 @@ namespace MWMechanics void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { - // Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. + // Check if an idle actor is too far from all allowed positions or too close to a door - if so start walking. storage.mCheckIdlePositionTimer += duration; if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary()) { storage.mCheckIdlePositionTimer = 0; // restart timer static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; - if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance)) + if (proximityToDoor(actor, distance) || !isNearAllowedPosition(actor, storage, distance)) { storage.setState(AiWanderStorage::Wander_MoveNow); - storage.mTrimCurrentNode = false; // just in case + storage.mTrimCurrentPosition = false; // just in case return; } } @@ -541,16 +542,14 @@ namespace MWMechanics } } - bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const + bool AiWander::isNearAllowedPosition( + const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const { const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) - { - osg::Vec3f point(node.mX, node.mY, node.mZ); - if ((actorPos - point).length2() < distance * distance) - return true; - } - return false; + const float squaredDistance = distance * distance; + return std::ranges::find_if(storage.mAllowedPositions, [&](const osg::Vec3& v) { + return (actorPos - v).length2() < squaredDistance; + }) != storage.mAllowedPositions.end(); } void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, @@ -610,8 +609,8 @@ namespace MWMechanics if (proximityToDoor(actor, distance)) { // remove allowed points then select another random destination - storage.mTrimCurrentNode = true; - trimAllowedNodes(mPathFinder.getPath(), storage.mAllowedNodes); + storage.mTrimCurrentPosition = true; + trimAllowedPositions(mPathFinder.getPath(), storage.mAllowedPositions); mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_MoveNow); @@ -630,42 +629,41 @@ namespace MWMechanics } } - void AiWander::setPathToAnAllowedNode( + void AiWander::setPathToAnAllowedPosition( const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { auto world = MWBase::Environment::get().getWorld(); auto& prng = world->getPrng(); - unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); - const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode]; + const std::size_t randomAllowedPositionIndex + = static_cast(Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng)); + const osg::Vec3f randomAllowedPosition = storage.mAllowedPositions[randomAllowedPositionIndex]; const osg::Vec3f start = actorPos.asVec3(); // don't take shortcuts for wandering const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); - const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest); - mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid)); + mPathFinder.buildPathByPathgrid(start, randomAllowedPosition, actor.getCell(), getPathGridGraph(pathgrid)); if (mPathFinder.isPathConstructed()) { - mDestination = destVec3f; + mDestination = randomAllowedPosition; mHasDestination = true; mUsePathgrid = true; - // Remove this node as an option and add back the previously used node (stops NPC from picking the same - // node): - ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode]; - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); - // check if mCurrentNode was taken out of mAllowedNodes - if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1) - storage.mTrimCurrentNode = false; + // Remove this position as an option and add back the previously used position (stops NPC from picking the + // same position): + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + // check if mCurrentPosition was taken out of mAllowedPositions + if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1) + storage.mTrimCurrentPosition = false; else - storage.mAllowedNodes.push_back(storage.mCurrentNode); - storage.mCurrentNode = temp; + storage.mAllowedPositions.push_back(storage.mCurrentPosition); + storage.mCurrentPosition = randomAllowedPosition; storage.setState(AiWanderStorage::Wander_Walking); } - // Choose a different node and delete this one from possible nodes because it is uncreachable: + // Choose a different position and delete this one from possible positions because it is uncreachable: else - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); } void AiWander::stopWalking(const MWWorld::Ptr& actor) @@ -741,20 +739,20 @@ namespace MWMechanics return; AiWanderStorage& storage = state.get(); - if (storage.mPopulateAvailableNodes) - getAllowedNodes(actor, storage); + if (storage.mPopulateAvailablePositions) + fillAllowedPositions(actor, storage); - if (storage.mAllowedNodes.empty()) + if (storage.mAllowedPositions.empty()) return; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); - ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index]; + int index = Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng); + const osg::Vec3f worldDest = storage.mAllowedPositions[index]; const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*actor.getCell()->getCell()); - ESM::Pathgrid::Point dest = converter.toLocalPoint(worldDest); + osg::Vec3f dest = converter.toLocalVec3(worldDest); - bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( - PathFinder::makeOsgVec3(worldDest), 60); + const bool isPathGridOccupied + = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(worldDest, 60); // add offset only if the selected pathgrid is occupied by another actor if (isPathGridOccupied) @@ -774,19 +772,17 @@ namespace MWMechanics const ESM::Pathgrid::Point& connDest = points[randomIndex]; // add an offset towards random neighboring node - osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest); - float length = dir.length(); + osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - dest; + const float length = dir.length(); dir.normalize(); for (int j = 1; j <= 3; j++) { // move for 5-15% towards random neighboring node - dest - = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f)); - worldDest = converter.toWorldPoint(dest); + dest = dest + dir * (j * 5 * length / 100.f); isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( - PathFinder::makeOsgVec3(worldDest), 60); + converter.toWorldVec3(dest), 60); if (!isOccupied) break; @@ -806,19 +802,18 @@ namespace MWMechanics // place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be // underground. Adding 20 in adjustPosition() is not enough. - dest.mZ += 60; + dest.z() += 60; converter.toWorld(dest); state.reset(); - osg::Vec3f pos(static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); - MWBase::Environment::get().getWorld()->moveObject(actor, pos); + MWBase::Environment::get().getWorld()->moveObject(actor, dest); actor.getClass().adjustPosition(actor, false); } void AiWander::getNeighbouringNodes( - ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) + const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*currentCell->getCell()); @@ -826,19 +821,19 @@ namespace MWMechanics if (pathgrid == nullptr || pathgrid->mPoints.empty()) return; - size_t index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); + const size_t index = PathFinder::getClosestPoint(pathgrid, dest); getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); } - void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage) + void AiWander::fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage) { // infrequently used, therefore no benefit in caching it as a member const MWWorld::CellStore* cellStore = actor.getCell(); const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*cellStore->getCell()); - storage.mAllowedNodes.clear(); + storage.mAllowedPositions.clear(); // If there is no path this actor doesn't go anywhere. See: // https://forum.openmw.org/viewtopic.php?t=1556 @@ -861,32 +856,33 @@ namespace MWMechanics // Find closest pathgrid point size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); - // mAllowedNodes for this actor with pathgrid point indexes based on mDistance + // mAllowedPositions for this actor with pathgrid point indexes based on mDistance // and if the point is connected to the closest current point // NOTE: mPoints is in local coordinates size_t pointIndex = 0; for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++) { - osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter])); + const osg::Vec3f nodePos = PathFinder::makeOsgVec3(pathgrid->mPoints[counter]); if ((npcPos - nodePos).length2() <= mDistance * mDistance && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter)) { - storage.mAllowedNodes.push_back(converter.toWorldPoint(pathgrid->mPoints[counter])); + storage.mAllowedPositions.push_back( + Misc::Convert::makeOsgVec3f(converter.toWorldPoint(pathgrid->mPoints[counter]))); pointIndex = counter; } } - if (storage.mAllowedNodes.size() == 1) + if (storage.mAllowedPositions.size() == 1) { - storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(mInitialActorPosition)); + storage.mAllowedPositions.push_back(mInitialActorPosition); addNonPathGridAllowedPoints(pathgrid, pointIndex, storage, converter); } - if (!storage.mAllowedNodes.empty()) + if (!storage.mAllowedPositions.empty()) { - setCurrentNodeToClosestAllowedNode(storage); + setCurrentPositionToClosestAllowedPosition(storage); } } - storage.mPopulateAvailableNodes = false; + storage.mPopulateAvailablePositions = false; } // When only one path grid point in wander distance, @@ -900,13 +896,13 @@ namespace MWMechanics { if (edge.mV0 == pointIndex) { - AddPointBetweenPathGridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), + addPositionBetweenPathgridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), converter.toWorldPoint(pathGrid->mPoints[edge.mV1]), storage); } } } - void AiWander::AddPointBetweenPathGridPoints( + void AiWander::addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) { osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); @@ -919,25 +915,25 @@ namespace MWMechanics // must not travel longer than distance between waypoints or NPC goes past waypoint distance = std::min(distance, static_cast(length)); delta *= distance; - storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); + storage.mAllowedPositions.push_back(vectorStart + delta); } - void AiWander::setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage) + void AiWander::setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage) { - float distanceToClosestNode = std::numeric_limits::max(); + float distanceToClosestPosition = std::numeric_limits::max(); size_t index = 0; - for (size_t i = 0; i < storage.mAllowedNodes.size(); ++i) + for (size_t i = 0; i < storage.mAllowedPositions.size(); ++i) { - osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[i])); - float tempDist = (mInitialActorPosition - nodePos).length2(); - if (tempDist < distanceToClosestNode) + const osg::Vec3f position = storage.mAllowedPositions[i]; + const float tempDist = (mInitialActorPosition - position).length2(); + if (tempDist < distanceToClosestPosition) { index = i; - distanceToClosestNode = tempDist; + distanceToClosestPosition = tempDist; } } - storage.mCurrentNode = storage.mAllowedNodes[index]; - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index); + storage.mCurrentPosition = storage.mAllowedPositions[index]; + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + index); } void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 0bb0f64a83..0518015936 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -51,14 +51,13 @@ namespace MWMechanics unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors - // do we need to calculate allowed nodes based on mDistance - bool mPopulateAvailableNodes; + bool mPopulateAvailablePositions; - // allowed pathgrid nodes based on mDistance from the spawn point - std::vector mAllowedNodes; + // allowed destination positions based on mDistance from the spawn point + std::vector mAllowedPositions; - ESM::Pathgrid::Point mCurrentNode; - bool mTrimCurrentNode; + osg::Vec3f mCurrentPosition; + bool mTrimCurrentPosition; float mCheckIdlePositionTimer; int mStuckCount; @@ -132,7 +131,8 @@ namespace MWMechanics bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); int getRandomIdle() const; - void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); + void setPathToAnAllowedPosition( + const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage); void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage); @@ -145,26 +145,27 @@ namespace MWMechanics void wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance); bool destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination); void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); - bool isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; + bool isNearAllowedPosition(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; - const unsigned mDistance; // how far the actor can wander from the spawn point + // how far the actor can wander from the spawn point + const unsigned mDistance; const unsigned mDuration; float mRemainingDuration; const int mTimeOfDay; const std::vector mIdle; bool mStoredInitialActorPosition; - osg::Vec3f - mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell + // Note: an original engine does not reset coordinates even when actor changes a cell + osg::Vec3f mInitialActorPosition; bool mHasDestination; osg::Vec3f mDestination; bool mUsePathgrid; void getNeighbouringNodes( - ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); + const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); - void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage); + void fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage); // constants for converting idleSelect values into groupNames enum GroupIndex @@ -173,12 +174,12 @@ namespace MWMechanics GroupIndex_MaxIdle = 9 }; - void setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage); + void setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage); void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage, const Misc::CoordinateConverter& converter); - void AddPointBetweenPathGridPoints( + void addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); /// lookup table for converting idleSelect value to groupName diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 94242404e4..b68532c9d4 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -145,19 +145,6 @@ namespace MWMechanics mPath.push_back(point); } - /// utility function to convert a osg::Vec3f to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v) - { - return ESM::Pathgrid::Point(static_cast(v[0]), static_cast(v[1]), static_cast(v[2])); - } - - /// utility function to convert an ESM::Position to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p) - { - return ESM::Pathgrid::Point( - static_cast(p.pos[0]), static_cast(p.pos[1]), static_cast(p.pos[2])); - } - static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p) { return osg::Vec3f(static_cast(p.mX), static_cast(p.mY), static_cast(p.mZ)); diff --git a/components/misc/coordinateconverter.hpp b/components/misc/coordinateconverter.hpp index 7853880809..6c4d8dbf71 100644 --- a/components/misc/coordinateconverter.hpp +++ b/components/misc/coordinateconverter.hpp @@ -59,10 +59,18 @@ namespace Misc point.y() -= static_cast(mCellY); } + osg::Vec3f toWorldVec3(const osg::Vec3f& point) const + { + osg::Vec3f result = point; + toWorld(result); + return result; + } + osg::Vec3f toLocalVec3(const osg::Vec3f& point) const { - return osg::Vec3f( - point.x() - static_cast(mCellX), point.y() - static_cast(mCellY), point.z()); + osg::Vec3f result = point; + toLocal(result); + return result; } private: From 927b2bcceb65ad95c30af3f8a2f1ead524f65493 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 15:34:18 +0200 Subject: [PATCH 11/33] Replace PathFinder::makeOsgVec3 by Misc::Convert::makeOsgVec3f --- apps/openmw/mwmechanics/aicombat.cpp | 3 ++- apps/openmw/mwmechanics/aiwander.cpp | 8 ++++---- apps/openmw/mwmechanics/pathfinding.cpp | 8 ++++---- apps/openmw/mwmechanics/pathfinding.hpp | 8 ++------ 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index a6f9935194..a769d85cdd 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -455,7 +455,8 @@ namespace MWMechanics float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); if ((dist > fFleeDistance && !storage.mLOS) - || pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration, supportedMovementDirections)) + || pathTo( + actor, Misc::Convert::makeOsgVec3f(storage.mFleeDest), duration, supportedMovementDirections)) { state = AiCombatStorage::FleeState_Idle; } diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index a2d304c2e0..dfa0bc41ac 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -772,7 +772,7 @@ namespace MWMechanics const ESM::Pathgrid::Point& connDest = points[randomIndex]; // add an offset towards random neighboring node - osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - dest; + osg::Vec3f dir = Misc::Convert::makeOsgVec3f(connDest) - dest; const float length = dir.length(); dir.normalize(); @@ -862,7 +862,7 @@ namespace MWMechanics size_t pointIndex = 0; for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++) { - const osg::Vec3f nodePos = PathFinder::makeOsgVec3(pathgrid->mPoints[counter]); + const osg::Vec3f nodePos = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[counter]); if ((npcPos - nodePos).length2() <= mDistance * mDistance && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter)) { @@ -905,8 +905,8 @@ namespace MWMechanics void AiWander::addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) { - osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); - osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart; + osg::Vec3f vectorStart = Misc::Convert::makeOsgVec3f(start); + osg::Vec3f delta = Misc::Convert::makeOsgVec3f(end) - vectorStart; float length = delta.length(); delta.normalize(); diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index dc9d8e4061..dcad29c907 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -223,7 +223,7 @@ namespace MWMechanics { ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]); converter.toWorld(temp); - *out++ = makeOsgVec3(temp); + *out++ = Misc::Convert::makeOsgVec3f(temp); } else { @@ -234,8 +234,8 @@ namespace MWMechanics if (path.size() > 1) { ESM::Pathgrid::Point secondNode = *(++path.begin()); - osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]); - osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode); + osg::Vec3f firstNodeVec3f = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[startNode]); + osg::Vec3f secondNodeVec3f = Misc::Convert::makeOsgVec3f(secondNode); osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f; osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f; if (toSecondNodeVec3f * toStartPointVec3f > 0) @@ -259,7 +259,7 @@ namespace MWMechanics // convert supplied path to world coordinates std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) { converter.toWorld(point); - return makeOsgVec3(point); + return Misc::Convert::makeOsgVec3f(point); }); } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index b68532c9d4..20ba85cc96 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace MWWorld { @@ -145,18 +146,13 @@ namespace MWMechanics mPath.push_back(point); } - static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p) - { - return osg::Vec3f(static_cast(p.mX), static_cast(p.mY), static_cast(p.mZ)); - } - // Slightly cheaper version for comparisons. // Caller needs to be careful for very short distances (i.e. less than 1) // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 // static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) { - return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2(); + return (Misc::Convert::makeOsgVec3f(point) - pos).length2(); } // Return the closest pathgrid point index from the specified position From 3f1ac1848ca3be0a28e0b4eaf4e2e07ae345db1a Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 15:50:07 +0200 Subject: [PATCH 12/33] Move getClosestPoint to a separate file --- apps/openmw/mwmechanics/aicombat.cpp | 16 ++++---- apps/openmw/mwmechanics/aicombat.hpp | 5 ++- apps/openmw/mwmechanics/aipackage.hpp | 2 + apps/openmw/mwmechanics/aiwander.cpp | 5 ++- apps/openmw/mwmechanics/aiwander.hpp | 11 ++--- apps/openmw/mwmechanics/pathfinding.cpp | 9 +++-- apps/openmw/mwmechanics/pathfinding.hpp | 42 +------------------- components/misc/pathgridutils.hpp | 53 +++++++++++++++++++++++++ 8 files changed, 81 insertions(+), 62 deletions(-) create mode 100644 components/misc/pathgridutils.hpp diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index a769d85cdd..7f6003d5b2 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -1,13 +1,11 @@ #include "aicombat.hpp" -#include -#include - -#include - -#include - #include +#include +#include +#include +#include +#include #include #include "../mwphysics/raycasting.hpp" @@ -393,8 +391,8 @@ namespace MWMechanics osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); coords.toLocal(localPos); - size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); - for (size_t i = 0; i < pathgrid->mPoints.size(); i++) + const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, localPos); + for (std::size_t i = 0; i < pathgrid->mPoints.size(); i++) { if (i != closestPointIndex && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i)) diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index d5a9c3464c..42baaf6349 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -2,12 +2,13 @@ #define GAME_MWMECHANICS_AICOMBAT_H #include "aitemporarybase.hpp" +#include "aitimer.hpp" +#include "movement.hpp" #include "typedaipackage.hpp" #include "../mwworld/cellstore.hpp" // for Doors -#include "aitimer.hpp" -#include "movement.hpp" +#include namespace ESM { diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index edb62c97c4..42aa62ffe3 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -16,6 +16,8 @@ namespace ESM { struct Cell; + struct Pathgrid; + namespace AiSequence { struct AiSequence; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index dfa0bc41ac..7ebf37158b 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -821,7 +822,7 @@ namespace MWMechanics if (pathgrid == nullptr || pathgrid->mPoints.empty()) return; - const size_t index = PathFinder::getClosestPoint(pathgrid, dest); + const size_t index = Misc::getClosestPoint(*pathgrid, dest); getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); } @@ -854,7 +855,7 @@ namespace MWMechanics const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition); // Find closest pathgrid point - size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); + const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, npcPos); // mAllowedPositions for this actor with pathgrid point indexes based on mDistance // and if the point is connected to the closest current point diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 0518015936..3e0b704524 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -1,14 +1,15 @@ #ifndef GAME_MWMECHANICS_AIWANDER_H #define GAME_MWMECHANICS_AIWANDER_H -#include "typedaipackage.hpp" - -#include -#include - #include "aitemporarybase.hpp" #include "aitimer.hpp" #include "pathfinding.hpp" +#include "typedaipackage.hpp" + +#include + +#include +#include namespace ESM { diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index dcad29c907..d8b17529ab 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -38,7 +39,7 @@ namespace // points to a quadtree may help for (size_t counter = 0; counter < grid->mPoints.size(); counter++) { - float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos); + float potentialDistBetween = Misc::distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < closestDistanceReachable) { // found a closer one @@ -197,7 +198,7 @@ namespace MWMechanics // point right behind the wall that is closer than any pathgrid // point outside the wall osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint)); - size_t startNode = getClosestPoint(pathgrid, startPointInLocalCoords); + const size_t startNode = Misc::getClosestPoint(*pathgrid, startPointInLocalCoords); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); std::pair endNode @@ -206,8 +207,8 @@ namespace MWMechanics // if it's shorter for actor to travel from start to end, than to travel from either // start or end to nearest pathgrid point, just travel from start to end. float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2(); - float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); - float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); + float endTolastNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); + float startTo1stNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2)) { *out++ = endPoint; diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 20ba85cc96..45827a25df 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -5,12 +5,11 @@ #include #include +#include + #include #include #include -#include -#include -#include namespace MWWorld { @@ -146,43 +145,6 @@ namespace MWMechanics mPath.push_back(point); } - // Slightly cheaper version for comparisons. - // Caller needs to be careful for very short distances (i.e. less than 1) - // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 - // - static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) - { - return (Misc::Convert::makeOsgVec3f(point) - pos).length2(); - } - - // Return the closest pathgrid point index from the specified position - // coordinates. NOTE: Does not check if there is a sensible way to get there - // (e.g. a cliff in front). - // - // NOTE: pos is expected to be in local coordinates, as is grid->mPoints - // - static size_t getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) - { - assert(grid && !grid->mPoints.empty()); - - float distanceBetween = distanceSquared(grid->mPoints[0], pos); - size_t closestIndex = 0; - - // TODO: if this full scan causes performance problems mapping pathgrid - // points to a quadtree may help - for (size_t counter = 1; counter < grid->mPoints.size(); counter++) - { - float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); - if (potentialDistBetween < distanceBetween) - { - distanceBetween = potentialDistBetween; - closestIndex = counter; - } - } - - return closestIndex; - } - private: bool mConstructed = false; std::deque mPath; diff --git a/components/misc/pathgridutils.hpp b/components/misc/pathgridutils.hpp new file mode 100644 index 0000000000..5ca58f4d08 --- /dev/null +++ b/components/misc/pathgridutils.hpp @@ -0,0 +1,53 @@ +#ifndef OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H +#define OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H + +#include "convert.hpp" + +#include + +#include + +#include + +namespace Misc +{ + // Slightly cheaper version for comparisons. + // Caller needs to be careful for very short distances (i.e. less than 1) + // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 + // + inline float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) + { + return (Misc::Convert::makeOsgVec3f(point) - pos).length2(); + } + + // Return the closest pathgrid point index from the specified position + // coordinates. NOTE: Does not check if there is a sensible way to get there + // (e.g. a cliff in front). + // + // NOTE: pos is expected to be in local coordinates, as is grid->mPoints + // + inline std::size_t getClosestPoint(const ESM::Pathgrid& grid, const osg::Vec3f& pos) + { + if (grid.mPoints.empty()) + throw std::invalid_argument("Pathgrid has no points"); + + float minDistance = distanceSquared(grid.mPoints[0], pos); + std::size_t closestIndex = 0; + + // TODO: if this full scan causes performance problems mapping pathgrid + // points to a quadtree may help + for (std::size_t i = 1; i < grid.mPoints.size(); ++i) + { + const float distance = distanceSquared(grid.mPoints[i], pos); + if (minDistance > distance) + { + minDistance = distance; + closestIndex = i; + } + } + + return closestIndex; + } +} + +#endif From 5dfda0090b7565bc0abf763b112dc03f57202fe1 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 12 Apr 2025 13:35:39 +0200 Subject: [PATCH 13/33] Remove redundant cell argument from build path functions Actor can provide a cell. --- apps/openmw/mwmechanics/aicombat.cpp | 8 ++++---- apps/openmw/mwmechanics/aipackage.cpp | 4 ++-- apps/openmw/mwmechanics/aiwander.cpp | 4 ++-- apps/openmw/mwmechanics/pathfinding.cpp | 20 ++++++++++---------- apps/openmw/mwmechanics/pathfinding.hpp | 12 ++++++------ 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 7f6003d5b2..295c2bb436 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -300,8 +300,8 @@ namespace MWMechanics const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); const auto& pathGridGraph = getPathGridGraph(pathgrid); - mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, - navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + mPathFinder.buildPath(actor, vActorPos, vTargetPos, pathGridGraph, agentBounds, navigatorFlags, areaCosts, + storage.mAttackRange, PathType::Full); if (!mPathFinder.isPathConstructed()) { @@ -314,8 +314,8 @@ namespace MWMechanics if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) { // If the point is close enough, try to find a path to that point. - mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, agentBounds, - navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + mPathFinder.buildPath(actor, vActorPos, *hit, pathGridGraph, agentBounds, navigatorFlags, areaCosts, + storage.mAttackRange, PathType::Full); if (mPathFinder.isPathConstructed()) { // If path to that point is found use it as custom destination. diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 4bcfc7dedd..b71c01397b 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -180,8 +180,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& = world->getStore().get().search(*actor.getCell()->getCell()); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, navigatorFlags, areaCosts, endTolerance, pathType); + mPathFinder.buildLimitedPath(actor, position, dest, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, pathType); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 7ebf37158b..40b8ee1a5a 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -253,8 +253,8 @@ namespace MWMechanics constexpr float endTolerance = 0; const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); + mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, PathType::Full); } if (mPathFinder.isPathConstructed()) diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index d8b17529ab..8a3d9116e0 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -391,12 +391,12 @@ namespace MWMechanics } void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType) { mPath.clear(); - mCell = cell; + mCell = actor.getCell(); DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; @@ -452,9 +452,9 @@ namespace MWMechanics } void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType) { const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto maxDistance @@ -462,9 +462,9 @@ namespace MWMechanics const auto startToEnd = endPoint - startPoint; const auto distance = startToEnd.length(); if (distance <= maxDistance) - return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts, - endTolerance, pathType); + return buildPath( + actor, startPoint, endPoint, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); const auto end = startPoint + startToEnd * maxDistance / distance; - buildPath(actor, startPoint, end, cell, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); + buildPath(actor, startPoint, end, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); } } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 45827a25df..77b2f83c12 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -111,14 +111,14 @@ namespace MWMechanics PathType pathType); void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); /// Remove front point if exist and within tolerance void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, From 4433e3c2de68cee080452b0548fd2a39c9963229 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 21 Apr 2025 16:03:10 +0200 Subject: [PATCH 14/33] Build path over navmesh for wandering actors Using a path over pathgrid as checkpoints. This allows to avoid having paths going through obstacles if they are placed over pathgrid points. --- .../detournavigator/navigator.cpp | 186 +++++++++++++++--- apps/openmw/mwlua/nearbybindings.cpp | 18 +- apps/openmw/mwmechanics/aipackage.cpp | 6 +- apps/openmw/mwmechanics/aiwander.cpp | 90 +++++---- apps/openmw/mwmechanics/pathfinding.cpp | 33 ++-- apps/openmw/mwmechanics/pathfinding.hpp | 11 +- components/detournavigator/findsmoothpath.hpp | 84 ++++++-- components/detournavigator/navigatorutils.hpp | 10 +- files/lua_api/openmw/nearby.lua | 1 + .../integration_tests/test_lua_api/global.lua | 1 + .../integration_tests/test_lua_api/menu.lua | 1 + .../integration_tests/test_lua_api/player.lua | 33 ++++ 12 files changed, 366 insertions(+), 108 deletions(-) diff --git a/apps/components_tests/detournavigator/navigator.cpp b/apps/components_tests/detournavigator/navigator.cpp index 0a78c33c9e..a6e1d26c79 100644 --- a/apps/components_tests/detournavigator/navigator.cpp +++ b/apps/components_tests/detournavigator/navigator.cpp @@ -139,7 +139,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) { - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::NavMeshNotFound); EXPECT_EQ(mPath, std::deque()); } @@ -147,7 +147,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception) { ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::StartPolygonNotFound); } @@ -156,7 +156,7 @@ namespace ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->removeAgent(mAgentBounds); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::StartPolygonNotFound); } @@ -172,7 +172,7 @@ namespace updateGuard.reset(); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -194,7 +194,7 @@ namespace updateGuard.reset(); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath; @@ -218,7 +218,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -237,7 +237,7 @@ namespace mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -265,7 +265,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -285,7 +285,7 @@ namespace mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -318,7 +318,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -386,7 +386,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -421,7 +421,7 @@ namespace mEnd.x() = 256; mEnd.z() = 300; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -453,8 +453,8 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ( - findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, + {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -487,8 +487,8 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ( - findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, + {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -520,7 +520,7 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -549,7 +549,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -577,7 +577,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -658,7 +658,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -781,7 +781,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -806,7 +806,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::PartialPath); EXPECT_THAT(mPath, @@ -834,7 +834,7 @@ namespace const float endTolerance = 1000.0f; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -979,6 +979,146 @@ namespace EXPECT_EQ(usedNavMeshTiles, 854); } + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_mountains) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_cliffs) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, -1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 8.66659259796142578125), + Vec3fEq(385.33331298828125, 79.33331298828125, 8.66659259796142578125), + Vec3fEq(430.666656494140625, 124.66664886474609375, 8.66659259796142578125), + Vec3fEq(463.999969482421875, 463.999969482421875, 8.66659259796142578125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_with_checkpoints) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const std::vector checkpoints = { + osg::Vec3f(400, 70, 12), + }; + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(400, 70, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_skip_unreachable_checkpoints) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const std::vector checkpoints = { + osg::Vec3f(400, 70, 10000), + osg::Vec3f(256, 256, 1000), + osg::Vec3f(-1000, -1000, 0), + }; + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam { }; diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index a6d762499a..6c244a0fd4 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -233,6 +233,7 @@ namespace MWLua DetourNavigator::Flags includeFlags = defaultIncludeFlags; DetourNavigator::AreaCosts areaCosts{}; float destinationTolerance = 1; + std::vector checkpoints; if (options.has_value()) { @@ -258,13 +259,24 @@ namespace MWLua } if (const auto& v = options->get>("destinationTolerance")) destinationTolerance = *v; + if (const auto& t = options->get>("checkpoints")) + { + for (const auto& [k, v] : *t) + { + const int index = k.as(); + const osg::Vec3f position = v.as(); + if (index != static_cast(checkpoints.size() + 1)) + throw std::runtime_error("checkpoints is not an array"); + checkpoints.push_back(position); + } + } } std::vector path; - const DetourNavigator::Status status - = DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, - source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path)); + const DetourNavigator::Status status = DetourNavigator::findPath( + *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination, + includeFlags, areaCosts, destinationTolerance, checkpoints, std::back_inserter(path)); sol::table result(lua, sol::create); LuaUtil::copyVectorToTable(path, result); diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index b71c01397b..3fcb28307c 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -501,7 +501,11 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld:: result |= DetourNavigator::Flag_swim; if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) - result |= DetourNavigator::Flag_walk | DetourNavigator::Flag_usePathgrid; + { + result |= DetourNavigator::Flag_walk; + if (getTypeId() != AiPackageTypeId::Wander) + result |= DetourNavigator::Flag_usePathgrid; + } if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 40b8ee1a5a..71cc8b5e7d 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -242,20 +242,12 @@ namespace MWMechanics { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*actor.getCell()->getCell()); - if (mUsePathgrid) - { - mPathFinder.buildPathByPathgrid( - pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid)); - } - else - { - const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); - constexpr float endTolerance = 0; - const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); - const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds, - navigatorFlags, areaCosts, endTolerance, PathType::Full); - } + const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); + constexpr float endTolerance = 0; + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); + mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, PathType::Full); if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); @@ -633,38 +625,64 @@ namespace MWMechanics void AiWander::setPathToAnAllowedPosition( const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { - auto world = MWBase::Environment::get().getWorld(); - auto& prng = world->getPrng(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + Misc::Rng::Generator& prng = world.getPrng(); const std::size_t randomAllowedPositionIndex = static_cast(Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng)); const osg::Vec3f randomAllowedPosition = storage.mAllowedPositions[randomAllowedPositionIndex]; const osg::Vec3f start = actorPos.asVec3(); - // don't take shortcuts for wandering - const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); - mPathFinder.buildPathByPathgrid(start, randomAllowedPosition, actor.getCell(), getPathGridGraph(pathgrid)); + const MWWorld::Cell& cell = *actor.getCell()->getCell(); + const ESM::Pathgrid* pathgrid = world.getStore().get().search(cell); + const PathgridGraph& pathgridGraph = getPathGridGraph(pathgrid); - if (mPathFinder.isPathConstructed()) - { - mDestination = randomAllowedPosition; - mHasDestination = true; - mUsePathgrid = true; - // Remove this position as an option and add back the previously used position (stops NPC from picking the - // same position): - storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); - // check if mCurrentPosition was taken out of mAllowedPositions - if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1) - storage.mTrimCurrentPosition = false; - else - storage.mAllowedPositions.push_back(storage.mCurrentPosition); - storage.mCurrentPosition = randomAllowedPosition; + const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(cell); + std::deque path + = pathgridGraph.aStarSearch(Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(start)), + Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(randomAllowedPosition))); - storage.setState(AiWanderStorage::Wander_Walking); - } // Choose a different position and delete this one from possible positions because it is uncreachable: - else + if (path.empty()) + { storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + return; + } + + // Drop nearest pathgrid point. + path.pop_front(); + + std::vector checkpoints(path.size()); + for (std::size_t i = 0; i < path.size(); ++i) + checkpoints[i] = Misc::Convert::makeOsgVec3f(converter.toWorldPoint(path[i])); + + const DetourNavigator::AgentBounds agentBounds = world.getPathfindingAgentBounds(actor); + const DetourNavigator::Flags flags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, flags); + constexpr float endTolerance = 0; + mPathFinder.buildPath(actor, start, randomAllowedPosition, pathgridGraph, agentBounds, flags, areaCosts, + endTolerance, PathType::Full, checkpoints); + + if (!mPathFinder.isPathConstructed()) + { + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + return; + } + + mDestination = randomAllowedPosition; + mHasDestination = true; + mUsePathgrid = true; + // Remove this position as an option and add back the previously used position (stops NPC from picking the + // same position): + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + // check if mCurrentPosition was taken out of mAllowedPositions + if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1) + storage.mTrimCurrentPosition = false; + else + storage.mAllowedPositions.push_back(storage.mCurrentPosition); + storage.mCurrentPosition = randomAllowedPosition; + + storage.setState(AiWanderStorage::Wander_Walking); } void AiWander::stopWalking(const MWWorld::Ptr& actor) diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 8a3d9116e0..165250c5c8 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -360,26 +360,16 @@ namespace MWMechanics mConstructed = true; } - void PathFinder::buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph) - { - mPath.clear(); - mCell = cell; - - buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); - - mConstructed = !mPath.empty(); - } - void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType, + std::span checkpoints) { mPath.clear(); // If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, - areaCosts, endTolerance, pathType, std::back_inserter(mPath)); + areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); @@ -393,7 +383,7 @@ namespace MWMechanics void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType) + PathType pathType, std::span checkpoints) { mPath.clear(); mCell = actor.getCell(); @@ -403,7 +393,7 @@ namespace MWMechanics if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance, - pathType, std::back_inserter(mPath)); + pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); } @@ -412,7 +402,7 @@ namespace MWMechanics && (flags & DetourNavigator::Flag_usePathgrid) == 0) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, - flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, + flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); @@ -430,12 +420,13 @@ namespace MWMechanics DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType, std::back_insert_iterator> out) + PathType pathType, std::span checkpoints, + std::back_insert_iterator> out) { - const auto world = MWBase::Environment::get().getWorld(); - const auto navigator = world->getNavigator(); - const auto status = DetourNavigator::findPath( - *navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out); + const MWBase::World& world = *MWBase::Environment::get().getWorld(); + const DetourNavigator::Navigator& navigator = *world.getNavigator(); + const DetourNavigator::Status status = DetourNavigator::findPath( + navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, checkpoints, out); if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) return DetourNavigator::Status::Success; diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 77b2f83c12..55064d9e88 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -102,18 +103,15 @@ namespace MWMechanics void buildStraightPath(const osg::Vec3f& endPoint); - void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph); - void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType); + PathType pathType, std::span checkpoints = {}); void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType); + PathType pathType, std::span checkpoints = {}); void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, @@ -156,7 +154,8 @@ namespace MWMechanics [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType, std::back_insert_iterator> out); + PathType pathType, std::span checkpoints, + std::back_insert_iterator> out); }; } diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp index e5efa8815f..d01b6bc1c7 100644 --- a/components/detournavigator/findsmoothpath.hpp +++ b/components/detournavigator/findsmoothpath.hpp @@ -13,7 +13,6 @@ #include -#include #include #include #include @@ -64,6 +63,25 @@ namespace DetourNavigator std::reference_wrapper mSettings; }; + template + class ToNavMeshCoordinatesSpan + { + public: + explicit ToNavMeshCoordinatesSpan(std::span span, const RecastSettings& settings) + : mSpan(span) + , mSettings(settings) + { + } + + std::size_t size() const noexcept { return mSpan.size(); } + + T operator[](std::size_t i) const noexcept { return toNavMeshCoordinates(mSettings, mSpan[i]); } + + private: + std::span mSpan; + const RecastSettings& mSettings; + }; + inline std::optional findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter, std::span pathBuffer) @@ -79,7 +97,7 @@ namespace DetourNavigator } Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end, - std::span polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, + std::span polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, bool skipFirst, std::output_iterator auto& out) { assert(polygonPathSize <= polygonPath.size()); @@ -95,7 +113,7 @@ namespace DetourNavigator dtStatusFailed(status)) return Status::FindStraightPathFailed; - for (int i = 0; i < cornersCount; ++i) + for (int i = skipFirst ? 1 : 0; i < cornersCount; ++i) *out++ = Misc::Convert::makeOsgVec3f(&cornerVertsBuffer[static_cast(i) * 3]); return Status::Success; @@ -103,7 +121,8 @@ namespace DetourNavigator Status findSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, const DetourSettings& settings, - float endTolerance, std::output_iterator auto out) + float endTolerance, const ToNavMeshCoordinatesSpan& checkpoints, + std::output_iterator auto out) { dtQueryFilter queryFilter; queryFilter.setIncludeFlags(includeFlags); @@ -131,29 +150,66 @@ namespace DetourNavigator return Status::EndPolygonNotFound; std::vector polygonPath(settings.mMaxPolygonPathSize); - const auto polygonPathSize - = findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath); + std::span polygonPathBuffer = polygonPath; + dtPolyRef currentRef = startRef; + osg::Vec3f currentNavMeshPos = startNavMeshPos; + bool skipFirst = false; - if (!polygonPathSize.has_value()) + for (std::size_t i = 0; i < checkpoints.size(); ++i) + { + const osg::Vec3f checkpointPos = checkpoints[i]; + osg::Vec3f checkpointNavMeshPos; + dtPolyRef checkpointRef; + if (const dtStatus status = navMeshQuery.findNearestPoly(checkpointPos.ptr(), polyHalfExtents.ptr(), + &queryFilter, &checkpointRef, checkpointNavMeshPos.ptr()); + dtStatusFailed(status) || checkpointRef == 0) + continue; + + const std::optional toCheckpointPathSize = findPolygonPath(navMeshQuery, currentRef, + checkpointRef, currentNavMeshPos, checkpointNavMeshPos, queryFilter, polygonPath); + + if (!toCheckpointPathSize.has_value()) + continue; + + if (*toCheckpointPathSize == 0) + continue; + + if (polygonPath[*toCheckpointPathSize - 1] != checkpointRef) + continue; + + const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, checkpointNavMeshPos, + polygonPath, *toCheckpointPathSize, settings.mMaxSmoothPathSize, skipFirst, out); + + if (smoothStatus != Status::Success) + return smoothStatus; + + currentRef = checkpointRef; + currentNavMeshPos = checkpointNavMeshPos; + skipFirst = true; + } + + const std::optional toEndPathSize = findPolygonPath( + navMeshQuery, currentRef, endRef, currentNavMeshPos, endNavMeshPos, queryFilter, polygonPathBuffer); + + if (!toEndPathSize.has_value()) return Status::FindPathOverPolygonsFailed; - if (*polygonPathSize == 0) - return Status::Success; + if (*toEndPathSize == 0) + return currentRef == endRef ? Status::Success : Status::PartialPath; osg::Vec3f targetNavMeshPos; if (const dtStatus status = navMeshQuery.closestPointOnPoly( - polygonPath[*polygonPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr); + polygonPath[*toEndPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr); dtStatusFailed(status)) return Status::TargetPolygonNotFound; - const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef; - const Status smoothStatus = makeSmoothPath(navMeshQuery, startNavMeshPos, targetNavMeshPos, polygonPath, - *polygonPathSize, settings.mMaxSmoothPathSize, out); + const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, targetNavMeshPos, polygonPath, + *toEndPathSize, settings.mMaxSmoothPathSize, skipFirst, out); if (smoothStatus != Status::Success) return smoothStatus; - return partialPath ? Status::PartialPath : Status::Success; + return polygonPath[*toEndPathSize - 1] == endRef ? Status::Success : Status::PartialPath; } } diff --git a/components/detournavigator/navigatorutils.hpp b/components/detournavigator/navigatorutils.hpp index ca02682ecd..d3b8b5e35a 100644 --- a/components/detournavigator/navigatorutils.hpp +++ b/components/detournavigator/navigatorutils.hpp @@ -11,6 +11,7 @@ #include #include +#include namespace DetourNavigator { @@ -21,13 +22,13 @@ namespace DetourNavigator * @param end path at given point. * @param includeFlags setup allowed navmesh areas. * @param out the beginning of the destination range. - * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents + * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents. + * @param checkpoints is a sequence of positions the path should go over if possible. * @return Status. - * Equal to out if no path is found. */ inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, float endTolerance, - std::output_iterator auto out) + std::span checkpoints, std::output_iterator auto out) { const auto navMesh = navigator.getNavMesh(agentBounds); if (navMesh == nullptr) @@ -37,7 +38,8 @@ namespace DetourNavigator const auto locked = navMesh->lock(); return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags, - areaCosts, settings.mDetour, endTolerance, outTransform); + areaCosts, settings.mDetour, endTolerance, ToNavMeshCoordinatesSpan(checkpoints, settings.mRecast), + outTransform); } /** diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index 29f1e79c24..a09e0cf50a 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -180,6 +180,7 @@ -- @field [parent=#FindPathOptions] #AreaCosts areaCosts a table defining relative cost for each type of area. -- @field [parent=#FindPathOptions] #number destinationTolerance a floating point number representing maximum allowed -- distance between destination and a nearest point on the navigation mesh in addition to agent size (default: 1). +-- @field [parent=#FindPathOptions] #table checkpoints an array of positions to build path over if possible. --- -- A table of parameters for @{#nearby.findRandomPointAroundCircle} and @{#nearby.castNavigationRay} diff --git a/scripts/data/integration_tests/test_lua_api/global.lua b/scripts/data/integration_tests/test_lua_api/global.lua index e5bfb5ef36..3a85e224d1 100644 --- a/scripts/data/integration_tests/test_lua_api/global.lua +++ b/scripts/data/integration_tests/test_lua_api/global.lua @@ -315,6 +315,7 @@ registerPlayerTest('player rotation') registerPlayerTest('player forward running') registerPlayerTest('player diagonal walking') registerPlayerTest('findPath') +registerPlayerTest('findPath with checkpoints') registerPlayerTest('findRandomPointAroundCircle') registerPlayerTest('castNavigationRay') registerPlayerTest('findNearestNavMeshPosition') diff --git a/scripts/data/integration_tests/test_lua_api/menu.lua b/scripts/data/integration_tests/test_lua_api/menu.lua index 3fc45bf2e7..8a2a278046 100644 --- a/scripts/data/integration_tests/test_lua_api/menu.lua +++ b/scripts/data/integration_tests/test_lua_api/menu.lua @@ -82,6 +82,7 @@ registerGlobalTest('player rotation', 'rotating player should not lead to nan ro registerGlobalTest('player forward running') registerGlobalTest('player diagonal walking') registerGlobalTest('findPath') +registerGlobalTest('findPath with checkpoints') registerGlobalTest('findRandomPointAroundCircle') registerGlobalTest('castNavigationRay') registerGlobalTest('findNearestNavMeshPosition') diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 0b481fba2b..88c4d2ee0f 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -179,6 +179,39 @@ testing.registerLocalTest('findPath', testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') testing.expectLessOrEqual((path[#path] - dst):length(), 1, 'Last path point ' .. testing.formatActualExpected(path[#path], dst)) + testing.expectThat(path, matchers.equalTo({ + matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1), + matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1), + })) + end) + +testing.registerLocalTest('findPath with checkpoints', + function() + local src = util.vector3(4096, 4096, 1745) + local dst = util.vector3(4500, 4500, 1745.95263671875) + local options = { + agentBounds = types.Actor.getPathfindingAgentBounds(self), + includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, + areaCosts = { + water = 1, + door = 2, + ground = 1, + pathgrid = 1, + }, + destinationTolerance = 1, + checkpoints = { + util.vector3(4200, 4100, 1750), + }, + } + local status, path = nearby.findPath(src, dst, options) + testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') + testing.expectLessOrEqual((path[#path] - dst):length(), 1, + 'Last path point ' .. testing.formatActualExpected(path[#path], dst)) + testing.expectThat(path, matchers.equalTo({ + matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1), + matchers.closeToVector(util.vector3(4200, 4100, 1749.5076904296875), 1e-1), + matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1), + })) end) testing.registerLocalTest('findRandomPointAroundCircle', From c5d74818eb485a84a55c77fc78ca2a782c985ab8 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 20 Jul 2025 11:09:20 +0200 Subject: [PATCH 15/33] Disable portability-template-virtual-member-function clang tidy warning There is no compatibility problem in practice. /home/elsid/dev/openmw/components/settings/sanitizer.hpp:11:19: error: unspecified virtual member function instantiation; the virtual member function is not instantiated but it might be with a different compiler [portability-template-virtual-member-function,-warnings-as-errors] 11 | virtual T apply(const T& value) const = 0; | ^ /home/elsid/dev/openmw/components/settings/sanitizerimpl.cpp:20:28: note: template instantiated here 20 | struct Max final : Sanitizer | ^ --- .clang-tidy | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-tidy b/.clang-tidy index 90c72765ca..8597ea5c13 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,6 +1,7 @@ Checks: > -*, portability-*, + -portability-template-virtual-member-function, clang-analyzer-*, -clang-analyzer-optin.*, -clang-analyzer-cplusplus.NewDeleteLeaks, From ec0c76d2f3cbbbc2af082155f46b6f5c6192494d Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 27 Jul 2025 12:11:21 +0200 Subject: [PATCH 16/33] Ignore false positive warning cellSize > 1 so the result of the division cannot be undefined. /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:18:35: error: The result of the '/' expression is undefined [clang-analyzer-core.UndefinedBinaryOperatorResult,-warnings-as-errors] 18 | std::size_t cell = global / (cellSize - 1); | ^ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:244:13: note: Assuming 'lodLevel' is >= 0 244 | if (lodLevel < 0 || 63 < lodLevel) | ^~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:244:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/storage.cpp:244:29: note: Assuming 'lodLevel' is <= 63 244 | if (lodLevel < 0 || 63 < lodLevel) | ^~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:244:9: note: Taking false branch 244 | if (lodLevel < 0 || 63 < lodLevel) | ^ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:247:13: note: Assuming 'size' is > 0 247 | if (size <= 0) | ^~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:247:9: note: Taking false branch 247 | if (size <= 0) | ^ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:272:13: note: Assuming the condition is false 272 | if (land != nullptr) | ^~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:272:9: note: Taking false branch 272 | if (land != nullptr) | ^ /home/elsid/dev/openmw/components/esmterrain/storage.cpp:363:9: note: Calling 'sampleCellGrid' 363 | sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, handleSample); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:72:13: note: Assuming 'cellSize' is >= 2 72 | if (cellSize < 2 || !Misc::isPowerOfTwo(cellSize - 1)) | ^~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:72:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:72:9: note: Taking false branch 72 | if (cellSize < 2 || !Misc::isPowerOfTwo(cellSize - 1)) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:75:13: note: Assuming 'sampleSize' is not equal to 0 75 | if (sampleSize == 0 || !Misc::isPowerOfTwo(sampleSize)) | ^~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:75:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:75:9: note: Taking false branch 75 | if (sampleSize == 0 || !Misc::isPowerOfTwo(sampleSize)) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:78:13: note: Assuming 'distance' is >= 2 78 | if (distance < 2 || !Misc::isPowerOfTwo(distance - 1)) | ^~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:78:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:78:9: note: Taking false branch 78 | if (distance < 2 || !Misc::isPowerOfTwo(distance - 1)) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:84:13: note: Assuming 'distance' is >= 'cellSize' 84 | if (distance < cellSize || sampleSize > cellSize - 1) | ^~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:84:13: note: Left side of '||' is false /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:84:36: note: Assuming the condition is true 84 | if (distance < cellSize || sampleSize > cellSize - 1) | ^~~~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:84:9: note: Taking true branch 84 | if (distance < cellSize || sampleSize > cellSize - 1) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:85:20: note: Calling 'sampleCellGridSimple' 85 | return sampleCellGridSimple(cellSize, sampleSize, beginX, beginY, endX, endY, f); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:56:16: note: 'cellSize' is > 1 56 | assert(cellSize > 1); | ^ /usr/include/assert.h:100:27: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:56:9: note: '?' condition is true 56 | assert(cellSize > 1); | ^ /usr/include/assert.h:100:7: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:57:9: note: '?' condition is true 57 | assert(Misc::isPowerOfTwo(cellSize - 1)); | ^ /usr/include/assert.h:100:7: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:58:16: note: 'sampleSize' is not equal to 0 58 | assert(sampleSize != 0); | ^ /usr/include/assert.h:100:27: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:58:9: note: '?' condition is true 58 | assert(sampleSize != 0); | ^ /usr/include/assert.h:100:7: note: expanded from macro 'assert' 100 | (static_cast (expr) \ | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:60:9: note: Calling 'sampleGrid<(lambda at /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:61:13)>' 60 | sampleGrid(sampleSize, beginX, beginY, endX, endY, | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 61 | [&](std::size_t globalX, std::size_t globalY, std::size_t vertX, std::size_t vertY) { | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | const auto [cellX, x] = toCellAndLocal(beginX, globalX, cellSize); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | const auto [cellY, y] = toCellAndLocal(beginY, globalY, cellSize); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | f(cellX, cellY, x, y, vertX, vertY); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 65 | }); | ~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:33:38: note: Assuming 'y' is < 'endY' 33 | for (std::size_t y = beginY; y < endY; y += sampleSize) | ^~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:33:9: note: Loop condition is true. Entering loop body 33 | for (std::size_t y = beginY; y < endY; y += sampleSize) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:36:42: note: Assuming 'x' is < 'endX' 36 | for (std::size_t x = beginX; x < endX; x += sampleSize) | ^~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:36:13: note: Loop condition is true. Entering loop body 36 | for (std::size_t x = beginX; x < endX; x += sampleSize) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:36:42: note: Assuming 'x' is < 'endX' 36 | for (std::size_t x = beginX; x < endX; x += sampleSize) | ^~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:36:13: note: Loop condition is true. Entering loop body 36 | for (std::size_t x = beginX; x < endX; x += sampleSize) | ^ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:37:17: note: Calling 'operator()' 37 | f(x, y, vertX++, vertY); | ^~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:62:41: note: Calling 'toCellAndLocal' 62 | const auto [cellX, x] = toCellAndLocal(beginX, globalX, cellSize); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /home/elsid/dev/openmw/components/esmterrain/gridsampling.hpp:18:35: note: The result of the '/' expression is undefined 18 | std::size_t cell = global / (cellSize - 1); | ~~~~~~~^~~~~~~~~~~~~~~~ --- components/esmterrain/gridsampling.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/esmterrain/gridsampling.hpp b/components/esmterrain/gridsampling.hpp index e71dfc5152..86544a214b 100644 --- a/components/esmterrain/gridsampling.hpp +++ b/components/esmterrain/gridsampling.hpp @@ -15,7 +15,9 @@ namespace ESMTerrain inline std::pair toCellAndLocal( std::size_t begin, std::size_t global, std::size_t cellSize) { + // NOLINTBEGIN(clang-analyzer-core.UndefinedBinaryOperatorResult) std::size_t cell = global / (cellSize - 1); + // NOLINTEND(clang-analyzer-core.UndefinedBinaryOperatorResult) std::size_t local = global & (cellSize - 2); if (global != begin && local == 0) { From 8682ea522f6c742e1ac1676e44ebc56207f72582 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 27 Jul 2025 12:20:10 +0200 Subject: [PATCH 17/33] Remove unused namespace alias --- apps/mwiniimporter/importer.cpp | 2 -- apps/mwiniimporter/main.cpp | 1 - 2 files changed, 3 deletions(-) diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index a8dee709da..4b8e7acd61 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -9,8 +9,6 @@ #include #include -namespace sfs = std::filesystem; - namespace { // from configfileparser.cpp diff --git a/apps/mwiniimporter/main.cpp b/apps/mwiniimporter/main.cpp index 6e4242cb4e..c5f21ac67f 100644 --- a/apps/mwiniimporter/main.cpp +++ b/apps/mwiniimporter/main.cpp @@ -10,7 +10,6 @@ #include namespace bpo = boost::program_options; -namespace sfs = std::filesystem; #ifndef _WIN32 int main(int argc, char* argv[]) From c6f381f1c4d050f9a98690f126680bd1a8621c84 Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 27 Jul 2025 12:21:13 +0200 Subject: [PATCH 18/33] Ignore readability-identifier-naming for boost::program_options namespace alias --- .clang-tidy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index 8597ea5c13..1f37003f8e 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -17,4 +17,4 @@ CheckOptions: - key: readability-identifier-naming.NamespaceCase value: CamelCase - key: readability-identifier-naming.NamespaceIgnoredRegexp - value: 'osg(DB|FX|Particle|Shadow|Viewer|Util)?' + value: 'bpo|osg(DB|FX|Particle|Shadow|Viewer|Util)?' From aae954643ce5cceafa023f4c7aa5961e66189136 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 27 Jul 2025 12:47:35 +0200 Subject: [PATCH 19/33] Don't multiply magnitudes for effects that don't have magnitudes --- apps/openmw/mwmechanics/spelleffects.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 99e5a09481..80dd67ef0d 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -377,8 +377,11 @@ namespace MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); return MWMechanics::MagicApplicationResult::Type::REMOVED; } - effect.mMinMagnitude *= magnitudeMult; - effect.mMaxMagnitude *= magnitudeMult; + else if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) + { + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } } return MWMechanics::MagicApplicationResult::Type::APPLIED; } From e77ee5c20fdd9d7e7966e588d58a02339aaa3c6c Mon Sep 17 00:00:00 2001 From: elsid Date: Sun, 27 Jul 2025 18:40:39 +0200 Subject: [PATCH 20/33] Do not copy cell store to count refs --- apps/openmw/mwworld/esmstore.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index ea183b6b53..7262805f81 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -566,10 +566,10 @@ namespace MWWorld std::vector refs; std::set keyIDs; std::vector refIDs; - Store Cells = get(); - for (auto it = Cells.intBegin(); it != Cells.intEnd(); ++it) + const Store& cells = get(); + for (auto it = cells.intBegin(); it != cells.intEnd(); ++it) readRefs(*it, refs, refIDs, keyIDs, readers); - for (auto it = Cells.extBegin(); it != Cells.extEnd(); ++it) + for (auto it = cells.extBegin(); it != cells.extEnd(); ++it) readRefs(*it, refs, refIDs, keyIDs, readers); const auto lessByRefNum = [](const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; std::stable_sort(refs.begin(), refs.end(), lessByRefNum); From ee501d8d0deb3c010b47e839ba848de3bfd6160c Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 27 Jul 2025 22:02:02 +0300 Subject: [PATCH 21/33] Remove some redundant class header includes --- apps/openmw/mwworld/class.hpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index d3d75aa935..7e49fad61b 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -6,7 +6,6 @@ #include #include -#include #include #include "doorstate.hpp" @@ -16,9 +15,13 @@ #include "../mwmechanics/damagesourcetype.hpp" #include -#include #include +namespace osg +{ + class Quat; +} + namespace ESM { struct ObjectState; From f6a6a33c5957c1685feced6709cc337f6f3e0b8b Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 27 Jul 2025 21:12:52 +0200 Subject: [PATCH 22/33] Prevent witnesses from ending combat because they didn't get hit --- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index c20061d022..a3e260a4a7 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1453,6 +1453,7 @@ namespace MWMechanics } startCombat(actor, player, &playerFollowers); + observerStats.setHitAttemptActorId(player.getClass().getCreatureStats(player).getActorId()); // Apply aggression value to the base Fight rating, so that the actor can continue fighting // after a Calm spell wears off From 272e6fabf94e3391340a7e9c1239fc21fd68b502 Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Sun, 27 Jul 2025 23:02:06 +0300 Subject: [PATCH 23/33] Turn GreetingState into enum class and cut some mechanics manager includes --- apps/openmw/mwbase/mechanicsmanager.hpp | 8 +++++--- apps/openmw/mwmechanics/actor.hpp | 2 +- apps/openmw/mwmechanics/actors.cpp | 19 ++++++++++--------- apps/openmw/mwmechanics/aitravel.cpp | 3 ++- apps/openmw/mwmechanics/aiwander.cpp | 5 +++-- apps/openmw/mwmechanics/greetingstate.hpp | 8 ++++---- apps/openmw/mwscript/aiextensions.cpp | 3 ++- 7 files changed, 27 insertions(+), 21 deletions(-) diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 23d79c1a6b..551d86a041 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -8,9 +8,6 @@ #include #include -#include "../mwmechanics/greetingstate.hpp" -#include "../mwrender/animationpriority.hpp" - #include "../mwworld/ptr.hpp" namespace osg @@ -27,6 +24,11 @@ namespace ESM class ESMWriter; } +namespace MWMechanics +{ + enum class GreetingState; +} + namespace MWWorld { class Ptr; diff --git a/apps/openmw/mwmechanics/actor.hpp b/apps/openmw/mwmechanics/actor.hpp index 69e370ec86..45c077f98c 100644 --- a/apps/openmw/mwmechanics/actor.hpp +++ b/apps/openmw/mwmechanics/actor.hpp @@ -73,7 +73,7 @@ namespace MWMechanics CharacterController mCharacterController; int mGreetingTimer{ 0 }; float mTargetAngleRadians{ 0.f }; - GreetingState mGreetingState{ Greet_None }; + GreetingState mGreetingState{ GreetingState::None }; Misc::DeviatingPeriodicTimer mEngageCombat{ 1.0f, 0.25f, Misc::Rng::deviate(0, 0.25f, MWBase::Environment::get().getWorld()->getPrng()) }; bool mIsTurningToPlayer{ false }; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 69108f9aff..4766afb55a 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -50,6 +50,7 @@ #include "attacktype.hpp" #include "character.hpp" #include "creaturestats.hpp" +#include "greetingstate.hpp" #include "movement.hpp" #include "npcstats.hpp" #include "steering.hpp" @@ -487,7 +488,7 @@ namespace MWMechanics { actorState.setTurningToPlayer(false); actorState.setGreetingTimer(0); - actorState.setGreetingState(Greet_None); + actorState.setGreetingState(GreetingState::None); return; } @@ -525,7 +526,7 @@ namespace MWMechanics int greetingTimer = actorState.getGreetingTimer(); GreetingState greetingState = actorState.getGreetingState(); - if (greetingState == Greet_None) + if (greetingState == GreetingState::None) { if ((playerPos - actorPos).length2() <= helloDistance * helloDistance && !playerStats.isDead() && !actorStats.isParalyzed() && !isTargetMagicallyHidden(player) @@ -535,14 +536,14 @@ namespace MWMechanics if (greetingTimer >= GREETING_SHOULD_START) { - greetingState = Greet_InProgress; + greetingState = GreetingState::InProgress; if (!MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("hello"))) - greetingState = Greet_Done; + greetingState = GreetingState::Done; greetingTimer = 0; } } - if (greetingState == Greet_InProgress) + if (greetingState == GreetingState::InProgress) { greetingTimer++; @@ -554,16 +555,16 @@ namespace MWMechanics if (greetingTimer >= GREETING_COOLDOWN) { - greetingState = Greet_Done; + greetingState = GreetingState::Done; greetingTimer = 0; } } - if (greetingState == Greet_Done) + if (greetingState == GreetingState::Done) { float resetDist = 2 * helloDistance; if ((playerPos - actorPos).length2() >= resetDist * resetDist) - greetingState = Greet_None; + greetingState = GreetingState::None; } actorState.setGreetingTimer(greetingTimer); @@ -2381,7 +2382,7 @@ namespace MWMechanics { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) - return Greet_None; + return GreetingState::None; return it->second->getGreetingState(); } diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index f0781565bf..a669de8339 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -12,6 +12,7 @@ #include "character.hpp" #include "creaturestats.hpp" +#include "greetingstate.hpp" #include "movement.hpp" namespace @@ -77,7 +78,7 @@ namespace MWMechanics if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump) && !stats.getMovementFlag(CreatureStats::Flag_ForceSneak) - && (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == Greet_InProgress)) + && (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == GreetingState::InProgress)) return false; const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 3cc7aac838..39a1b15300 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -24,6 +24,7 @@ #include "actorutil.hpp" #include "character.hpp" #include "creaturestats.hpp" +#include "greetingstate.hpp" #include "movement.hpp" #include "pathgrid.hpp" @@ -238,7 +239,7 @@ namespace MWMechanics && !cStats.getMovementFlag(CreatureStats::Flag_ForceSneak)) { GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); - if (greetingState == Greet_InProgress) + if (greetingState == GreetingState::InProgress) { if (storage.mState == AiWanderStorage::Wander_Walking) { @@ -508,7 +509,7 @@ namespace MWMechanics // Check if idle animation finished GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); - if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None)) + if (!checkIdle(actor, storage.mIdleAnimation) && greetingState != GreetingState::InProgress) { if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); diff --git a/apps/openmw/mwmechanics/greetingstate.hpp b/apps/openmw/mwmechanics/greetingstate.hpp index 9b37096322..4a5a4aa2f8 100644 --- a/apps/openmw/mwmechanics/greetingstate.hpp +++ b/apps/openmw/mwmechanics/greetingstate.hpp @@ -3,11 +3,11 @@ namespace MWMechanics { - enum GreetingState + enum class GreetingState { - Greet_None, - Greet_InProgress, - Greet_Done + None, + InProgress, + Done }; } diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index a91a585367..e5aa3b1f91 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -22,6 +22,7 @@ #include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/aiwander.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/greetingstate.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -487,7 +488,7 @@ namespace MWScript else if (testedTargetId == "Player") // Currently the player ID is hardcoded { MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); - bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress; + bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::GreetingState::InProgress; bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor); targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor); } From 395f6811c91f0b3a4b16e848d6a5419bc181af4f Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 28 Jul 2025 18:59:23 +0200 Subject: [PATCH 24/33] Remove minimum duration from continuous effects and add clarification to the docs --- CMakeLists.txt | 2 +- apps/openmw/mwlua/magicbindings.cpp | 3 --- files/lua_api/openmw/types.lua | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 78d9aae646..045808c84b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 50) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 83) +set(OPENMW_LUA_API_REVISION 84) set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_VERSION_COMMITHASH "") diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 19dada74a7..d2afde9536 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -976,9 +976,6 @@ namespace MWLua bool hasDuration = !(mgef->mData.mFlags & ESM::MagicEffect::NoDuration); effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; - bool appliedOnce = mgef->mData.mFlags & ESM::MagicEffect::AppliedOnce; - if (!appliedOnce) - effect.mDuration = std::max(1.f, effect.mDuration); effect.mTimeLeft = effect.mDuration; params.getEffects().emplace_back(effect); diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 2481c0ae75..b8c4aebe97 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -331,6 +331,7 @@ --- -- Adds a new spell to the list of active spells (only in global scripts or on self). -- Note that this does not play any related VFX or sounds. +-- Note that this should not be used to add spells without durations (i.e. abilities, curses, and diseases) as they will expire instantly. Use @{#ActorSpells.add} instead. -- @function [parent=#ActorActiveSpells] add -- @param self -- @param #table options A table of parameters. Must contain the following required parameters: From 70207750f287cfa00b24e84ce4a2244c2b9c6128 Mon Sep 17 00:00:00 2001 From: elsid Date: Mon, 28 Jul 2025 20:23:45 +0200 Subject: [PATCH 25/33] Make tests more stable --- apps/components_tests/detournavigator/navigator.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/components_tests/detournavigator/navigator.cpp b/apps/components_tests/detournavigator/navigator.cpp index a6e1d26c79..d2b48ce623 100644 --- a/apps/components_tests/detournavigator/navigator.cpp +++ b/apps/components_tests/detournavigator/navigator.cpp @@ -985,7 +985,7 @@ namespace 0, 0, 0, 0, 0, // row 0 0, 0, 0, 0, 0, // row 1 0, 0, 1000, 0, 0, // row 2 - 0, 0, 0, 0, 0, // row 3 + 0, 0, 1000, 0, 0, // row 3 0, 0, 0, 0, 0, // row 4 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); @@ -1017,7 +1017,7 @@ namespace 0, 0, 0, 0, 0, // row 0 0, 0, 0, 0, 0, // row 1 0, 0, -1000, 0, 0, // row 2 - 0, 0, 0, 0, 0, // row 3 + 0, 0, -1000, 0, 0, // row 3 0, 0, 0, 0, 0, // row 4 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); @@ -1049,7 +1049,7 @@ namespace 0, 0, 0, 0, 0, // row 0 0, 0, 0, 0, 0, // row 1 0, 0, 1000, 0, 0, // row 2 - 0, 0, 0, 0, 0, // row 3 + 0, 0, 1000, 0, 0, // row 3 0, 0, 0, 0, 0, // row 4 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); @@ -1086,7 +1086,7 @@ namespace 0, 0, 0, 0, 0, // row 0 0, 0, 0, 0, 0, // row 1 0, 0, 1000, 0, 0, // row 2 - 0, 0, 0, 0, 0, // row 3 + 0, 0, 1000, 0, 0, // row 3 0, 0, 0, 0, 0, // row 4 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); From ebcacf04bc02afe406155aa3f85261142d7c763d Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 28 Jul 2025 04:22:24 +0300 Subject: [PATCH 26/33] Sync changelog for 0.50.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5435835606..a2df2e025e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ------ Bug #2967: Inventory windows don't update when changing items by script + Bug #4437: Transformations for NiSkinInstance are ignored + Bug #4885: Disable in dialogue result script causes a crash Bug #5331: Pathfinding works incorrectly when actor is moved from one interior cell to another Bug #6039: Next Spell keybind fails while selected enchanted item has multiple copies Bug #6573: Editor: Selection behaves incorrectly on high-DPI displays @@ -9,9 +11,11 @@ Bug #7371: Equipping item from inventory does not play a Down sound when equipping fails Bug #7622: Player's marksman weapons don't work on close actors underwater Bug #7649: The sound and vfx of resisted enchanted items' magic still play + Bug #7693: I.ItemUsage should return an item to the selected stack if equipping/consumption is denied Bug #7740: Magic items in the HUD aren't composited correctly Bug #7799: Picking up ingredients while object paging active grid is on may cause a hiccup Bug #7871: Kwama Queen doesn't start combat with player + Bug #7979: Paralyzed NPCs battlecry Bug #8245: The console command ShowVars does not list global mwscripts Bug #8265: Topics are linked incorrectly Bug #8303: On target spells cast by non-actors should fire underwater @@ -25,30 +29,40 @@ Bug #8375: Moon phase cycle doesn't match Morrowind Bug #8383: Casting bound helm or boots on beast races doesn't cleanup properly Bug #8385: Russian encoding broken with locale parameters and calendar + Bug #8404: Prevent merchant equipping breaks on lights Bug #8408: OpenMW doesn't report all the potential resting hindrances Bug #8414: Waterwalking works when collision is disabled Bug #8431: Behaviour of removed items from a container is buggy Bug #8432: Changing to and from an interior cell doesn't update collision + Bug #8433: Wandering NPCs are not capable of avoiding easy obstacles Bug #8436: Spell selection in a pinned spellbook window doesn't update Bug #8437: Pinned inventory window's pin button doesn't look pressed Bug #8446: Travel prices are strangely inconsistent + Bug #8447: Werewolf swimming animation breaks in third person perspective Bug #8459: Changing magic effect base cost doesn't change spell price Bug #8466: Showmap "" reveals nameless cells Bug #8485: Witchwither disease and probably other common diseases don't work correctly Bug #8490: Normals on Water disappear when Water Shader is Enabled but Refraction is Disabled Bug #8500: OpenMW Alarm behaviour doesn't match morrowind.exe Bug #8519: Multiple bounty is sometimes assigned to player when detected during a pickpocketing action + Bug #8540: Magic resistance is applied to effects without a magnitude + Bug #8557: Charm's disposition changes capped on 100, uncapped below 0 + Bug #8582: addScript-attached local scripts start out inactive Bug #8585: Dialogue topic list doesn't have enough padding Bug #8587: Minor INI importer problems Bug #8593: Render targets do not generate mipmaps Bug #8598: Post processing shaders don't interact with the vfs correctly Bug #8599: Non-ASCII paths in BSA files don't work + Bug #8606: Floating point imprecision can mess with container capacity Bug #8609: The crosshair is too large Bug #8610: Terrain normal maps using NormalGL format instead of NormalDX Bug #8612: Using aiactivate on an ingredient when graphical herbalism is enabled triggers non-stop pickup sounds + Bug #8614: Lua garbage collection fails to remove unused data Bug #8615: Rest/wait time progress speed is different from vanilla Feature #2522: Support quick item transfer Feature #3769: Allow GetSpellEffects on enchantments + Feature #6976: [Lua] Weather API + Feature #8077: Save settings changes when clicking "ok"/closing the window Feature #8112: Expose landscape record data to Lua Feature #8113: Support extended selection in autodetected subdirectory dialog Feature #8139: Editor: Redesign the selection markers @@ -57,8 +71,10 @@ Feature #8320: Add access mwscript source text to lua api Feature #8334: Lua: AddTopic equivalent Feature #8355: Lua: Window visibility checking in interfaces.UI + Feature #8509: FillJournal script instruction Feature #8580: Sort characters in the save loading menu Feature #8597: Lua: Add more built-in event handlers + Feature #8629: Expose path grid data to Lua 0.49.0 ------ From b7fe1a6eb2599d712a59b4238627dad3bf0878eb Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 29 Jul 2025 11:59:42 -0700 Subject: [PATCH 27/33] Edit install-openmw.rst --- docs/source/manuals/installation/install-openmw.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index c99b04da91..f657638138 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -62,6 +62,19 @@ However, it depends on several packages which are not in stable, so it is not possible to install OpenMW in Wheezy without creating a FrankenDebian. This is not recommended or supported. +Fedora +====== + +OpenMW is available in the official repository of Fedora for versions 41 and up. +To install simply run the following as root (or in sudo), depending on what packages +you want. + +.. code-block:: console + + $ dnf install openmw + $ dnf install openmw-cs + $ dnf install openmw-tools + Flatpak ======= From aefa0ec1c4767485212757f8eb922d6588a7414a Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 29 Jun 2025 10:45:48 +0200 Subject: [PATCH 28/33] Add multiline (un)indent behaviour to the script editor --- apps/opencs/view/world/scriptedit.cpp | 89 +++++++++++++++++++++------ apps/opencs/view/world/scriptedit.hpp | 1 + 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/apps/opencs/view/world/scriptedit.cpp b/apps/opencs/view/world/scriptedit.cpp index 2871e0ced1..a846d4b342 100644 --- a/apps/opencs/view/world/scriptedit.cpp +++ b/apps/opencs/view/world/scriptedit.cpp @@ -21,6 +21,24 @@ #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" +namespace +{ + void prependToEachLine(QTextCursor begin, const QString& text) + { + QTextCursor end = begin; + begin.setPosition(begin.selectionStart()); + begin.movePosition(QTextCursor::StartOfLine); + end.setPosition(end.selectionEnd()); + end.movePosition(QTextCursor::EndOfLine); + begin.beginEditBlock(); + for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) + { + begin.insertText(text); + } + begin.endEditBlock(); + } +} + CSVWorld::ScriptEdit::ChangeLock::ChangeLock(ScriptEdit& edit) : mEdit(edit) { @@ -46,6 +64,55 @@ bool CSVWorld::ScriptEdit::event(QEvent* event) return QPlainTextEdit::event(event); } +void CSVWorld::ScriptEdit::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Backtab) + { + QTextCursor cursor = textCursor(); + QTextCursor end = cursor; + cursor.setPosition(cursor.selectionStart()); + cursor.movePosition(QTextCursor::StartOfLine); + end.setPosition(end.selectionEnd()); + end.movePosition(QTextCursor::EndOfLine); + cursor.beginEditBlock(); + for (; cursor < end; cursor.movePosition(QTextCursor::EndOfLine), cursor.movePosition(QTextCursor::Right)) + { + cursor.select(QTextCursor::LineUnderCursor); + QString line = cursor.selectedText(); + + if (line.isEmpty()) + continue; + qsizetype index = 0; + if (line[0] == '\t') + index = 1; + else + { + // Remove up to a tab worth of spaces instead + while (line[index].isSpace() && index < mTabCharCount && line[index] != '\t') + index++; + } + + if (index != 0) + { + line.remove(0, index); + cursor.insertText(line); + } + } + cursor.endEditBlock(); + return; + } + else if (event->key() == Qt::Key_Tab) + { + QTextCursor cursor = textCursor(); + if (cursor.hasSelection()) + { + prependToEachLine(cursor, "\t"); + return; + } + } + QPlainTextEdit::keyPressEvent(event); +} + CSVWorld::ScriptEdit::ScriptEdit(const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent) : QPlainTextEdit(parent) , mChangeLocked(0) @@ -316,22 +383,7 @@ void CSVWorld::ScriptEdit::markOccurrences() void CSVWorld::ScriptEdit::commentSelection() { - QTextCursor begin = textCursor(); - QTextCursor end = begin; - begin.setPosition(begin.selectionStart()); - begin.movePosition(QTextCursor::StartOfLine); - - end.setPosition(end.selectionEnd()); - end.movePosition(QTextCursor::EndOfLine); - - begin.beginEditBlock(); - - for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) - { - begin.insertText(";"); - } - - begin.endEditBlock(); + prependToEachLine(textCursor(), ";"); } void CSVWorld::ScriptEdit::uncommentSelection() @@ -345,17 +397,16 @@ void CSVWorld::ScriptEdit::uncommentSelection() end.movePosition(QTextCursor::EndOfLine); begin.beginEditBlock(); - for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) { begin.select(QTextCursor::LineUnderCursor); QString line = begin.selectedText(); - if (line.size() == 0) + if (line.isEmpty()) continue; // get first nonspace character in line - int index; + qsizetype index; for (index = 0; index != line.size(); ++index) { if (!line[index].isSpace()) diff --git a/apps/opencs/view/world/scriptedit.hpp b/apps/opencs/view/world/scriptedit.hpp index 53fa88ced3..d44c29eaab 100644 --- a/apps/opencs/view/world/scriptedit.hpp +++ b/apps/opencs/view/world/scriptedit.hpp @@ -74,6 +74,7 @@ namespace CSVWorld protected: bool event(QEvent* event) override; + void keyPressEvent(QKeyEvent* e) override; public: ScriptEdit(const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent); From 27ee192354a943c4f3282a4227486a4dc6ce5c8e Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 29 Jun 2025 10:57:23 +0200 Subject: [PATCH 29/33] Set default hotkeys for (un)commenting script lines --- apps/opencs/model/prefs/values.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/opencs/model/prefs/values.hpp b/apps/opencs/model/prefs/values.hpp index 16e5434d6b..7b6d5e9f5f 100644 --- a/apps/opencs/model/prefs/values.hpp +++ b/apps/opencs/model/prefs/values.hpp @@ -507,8 +507,10 @@ namespace CSMPrefs Settings::SettingValue mOrbitRollRight{ mIndex, sName, "orbit-roll-right", "E" }; Settings::SettingValue mOrbitSpeedMode{ mIndex, sName, "orbit-speed-mode", "" }; Settings::SettingValue mOrbitCenterSelection{ mIndex, sName, "orbit-center-selection", "C" }; - Settings::SettingValue mScriptEditorComment{ mIndex, sName, "script-editor-comment", "" }; - Settings::SettingValue mScriptEditorUncomment{ mIndex, sName, "script-editor-uncomment", "" }; + Settings::SettingValue mScriptEditorComment{ mIndex, sName, "script-editor-comment", + "Ctrl+Slash" }; + Settings::SettingValue mScriptEditorUncomment{ mIndex, sName, "script-editor-uncomment", + "Ctrl+Shift+Question" }; }; struct ModelsCategory : Settings::WithIndex From 442e8796b47fa80eac131caa2324271aecd05e96 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 30 Jul 2025 09:08:56 -0700 Subject: [PATCH 30/33] Edit install-openmw.rst --- docs/source/manuals/installation/install-openmw.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index f657638138..6cdc42fe72 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -69,6 +69,12 @@ OpenMW is available in the official repository of Fedora for versions 41 and up. To install simply run the following as root (or in sudo), depending on what packages you want. +``openmw`` includes the launcher, install wizard, iniimporter and the game itself. + +``openmw-cs`` includes the construction set. + +``openmw-tools`` includes `bsatool`, `esmtool` and `niftest`. + .. code-block:: console $ dnf install openmw From 62492c7738b7859673b7c81111d495ae44b72b8d Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 30 Jul 2025 09:11:00 -0700 Subject: [PATCH 31/33] Edit install-openmw.rst --- docs/source/manuals/installation/install-openmw.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index 6cdc42fe72..96c29b070c 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -73,7 +73,7 @@ you want. ``openmw-cs`` includes the construction set. -``openmw-tools`` includes `bsatool`, `esmtool` and `niftest`. +``openmw-tools`` includes ``bsatool``, ``esmtool`` and ``niftest``. .. code-block:: console From 24c7d2f075a4faeae9e7f6263406da2664360c81 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 30 Jul 2025 09:54:54 -0700 Subject: [PATCH 32/33] Edit install-openmw.rst --- docs/source/manuals/installation/install-openmw.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index 96c29b070c..05ea40a638 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -69,7 +69,7 @@ OpenMW is available in the official repository of Fedora for versions 41 and up. To install simply run the following as root (or in sudo), depending on what packages you want. -``openmw`` includes the launcher, install wizard, iniimporter and the game itself. +``openmw`` includes the launcher, install wizard, iniimporter and the engine itself. ``openmw-cs`` includes the construction set. From 34eb3d485d5eedf3fa038fd88c8369a54052e2b6 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Wed, 30 Jul 2025 20:21:15 +0000 Subject: [PATCH 33/33] [Lua] Partially dehardcode onHit --- CMakeLists.txt | 2 +- apps/launcher/settingspage.cpp | 4 - apps/launcher/ui/settingspage.ui | 28 +- apps/openmw/mwbase/luamanager.hpp | 10 + apps/openmw/mwbase/world.hpp | 3 - apps/openmw/mwclass/armor.cpp | 54 ++- apps/openmw/mwclass/armor.hpp | 5 +- apps/openmw/mwclass/clothing.cpp | 2 +- apps/openmw/mwclass/clothing.hpp | 2 +- apps/openmw/mwclass/creature.cpp | 85 ++-- apps/openmw/mwclass/creature.hpp | 9 +- apps/openmw/mwclass/npc.cpp | 174 +++---- apps/openmw/mwclass/npc.hpp | 9 +- apps/openmw/mwclass/weapon.cpp | 2 +- apps/openmw/mwclass/weapon.hpp | 2 +- apps/openmw/mwgui/inventorywindow.cpp | 7 +- apps/openmw/mwlua/corebindings.cpp | 3 + apps/openmw/mwlua/localscripts.hpp | 14 + apps/openmw/mwlua/luamanagerimp.cpp | 56 ++- apps/openmw/mwlua/luamanagerimp.hpp | 6 + apps/openmw/mwlua/types/actor.cpp | 36 ++ apps/openmw/mwlua/types/creature.cpp | 1 + apps/openmw/mwlua/types/npc.cpp | 1 + apps/openmw/mwmechanics/combat.cpp | 9 +- apps/openmw/mwmechanics/spelleffects.cpp | 3 +- apps/openmw/mwworld/class.cpp | 16 +- apps/openmw/mwworld/class.hpp | 18 +- apps/openmw/mwworld/inventorystore.cpp | 6 +- apps/openmw/mwworld/refdata.hpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 18 - apps/openmw/mwworld/worldimp.hpp | 3 - components/lua/scriptscontainer.hpp | 23 + components/settings/categories/game.hpp | 2 - docs/source/luadoc_data_paths.sh | 1 + .../source/reference/lua-scripting/events.rst | 61 ++- .../lua-scripting/interface_combat.rst | 8 + .../lua-scripting/tables/interfaces.rst | 3 + .../source/reference/modding/settings/GUI.rst | 1 - .../reference/modding/settings/game.rst | 11 - files/data/CMakeLists.txt | 7 + files/data/builtin.omwscripts | 3 + files/data/l10n/OMWCombat/en.yaml | 16 + files/data/l10n/OMWCombat/ru.yaml | 16 + files/data/l10n/OMWCombat/sv.yaml | 16 + files/data/scripts/omw/combat/common.lua | 41 ++ files/data/scripts/omw/combat/global.lua | 1 + files/data/scripts/omw/combat/local.lua | 431 ++++++++++++++++++ files/data/scripts/omw/combat/menu.lua | 1 + .../scripts/omw/mechanics/actorcontroller.lua | 13 + .../omw/mechanics/globalcontroller.lua | 11 + files/lang/launcher_de.ts | 8 - files/lang/launcher_en.ts | 8 - files/lang/launcher_fr.ts | 8 - files/lang/launcher_ru.ts | 8 - files/lang/launcher_sv.ts | 8 - files/lua_api/openmw/core.lua | 5 + files/lua_api/openmw/interfaces.lua | 3 + files/lua_api/openmw/types.lua | 2 + files/settings-default.cfg | 3 - 59 files changed, 978 insertions(+), 331 deletions(-) create mode 100644 docs/source/reference/lua-scripting/interface_combat.rst create mode 100644 files/data/l10n/OMWCombat/en.yaml create mode 100644 files/data/l10n/OMWCombat/ru.yaml create mode 100644 files/data/l10n/OMWCombat/sv.yaml create mode 100644 files/data/scripts/omw/combat/common.lua create mode 100644 files/data/scripts/omw/combat/global.lua create mode 100644 files/data/scripts/omw/combat/local.lua create mode 100644 files/data/scripts/omw/combat/menu.lua diff --git a/CMakeLists.txt b/CMakeLists.txt index db79dc2767..0d8e3cade9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 50) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 84) +set(OPENMW_LUA_API_REVISION 85) set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_VERSION_COMMITHASH "") diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index dfddc45bc5..27fa2ca27c 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -163,8 +163,6 @@ bool Launcher::SettingsPage::loadSettings() loadSettingInt(Settings::physics().mAsyncNumThreads, *physicsThreadsSpinBox); loadSettingBool( Settings::game().mAllowActorsToFollowOverWaterSurface, *allowNPCToFollowOverWaterSurfaceCheckBox); - loadSettingBool( - Settings::game().mUnarmedCreatureAttacksDamageArmor, *unarmedCreatureAttacksDamageArmorCheckBox); loadSettingInt(Settings::game().mActorCollisionShapeType, *actorCollisonShapeTypeComboBox); } @@ -373,8 +371,6 @@ void Launcher::SettingsPage::saveSettings() saveSettingInt(*physicsThreadsSpinBox, Settings::physics().mAsyncNumThreads); saveSettingBool( *allowNPCToFollowOverWaterSurfaceCheckBox, Settings::game().mAllowActorsToFollowOverWaterSurface); - saveSettingBool( - *unarmedCreatureAttacksDamageArmorCheckBox, Settings::game().mUnarmedCreatureAttacksDamageArmor); saveSettingInt(*actorCollisonShapeTypeComboBox, Settings::game().mActorCollisionShapeType); } diff --git a/apps/launcher/ui/settingspage.ui b/apps/launcher/ui/settingspage.ui index e792ac2843..b96d734605 100644 --- a/apps/launcher/ui/settingspage.ui +++ b/apps/launcher/ui/settingspage.ui @@ -53,7 +53,7 @@ - + <html><head/><body><p>Don't use race weight in NPC movement speed calculations.</p></body></html> @@ -63,7 +63,7 @@ - + <html><head/><body><p>Stops combat with NPCs affected by Calm spells every frame -- like in Morrowind without the MCP.</p></body></html> @@ -73,7 +73,7 @@ - + <html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html> @@ -123,7 +123,7 @@ - + <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> @@ -133,7 +133,7 @@ - + <html><head/><body><p>If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html> @@ -143,7 +143,7 @@ - + <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> @@ -153,7 +153,7 @@ - + <html><head/><body><p>Make enchanted weapons without Magical flag bypass normal weapons resistance, like in Morrowind.</p></body></html> @@ -183,7 +183,7 @@ - + <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> @@ -203,7 +203,7 @@ - + <html><head/><body><p>Effects of reflected Absorb spells are not mirrored - like in Morrowind.</p></body></html> @@ -213,16 +213,6 @@ - - - - <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 - - - diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index bbdb843199..61574de3ac 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -9,6 +9,7 @@ #include #include "../mwgui/mode.hpp" +#include "../mwmechanics/damagesourcetype.hpp" #include "../mwrender/animationpriority.hpp" #include @@ -39,6 +40,11 @@ namespace LuaUtil } } +namespace osg +{ + class Vec3f; +} + namespace MWBase { // \brief LuaManager is the central interface through which the engine invokes lua scripts. @@ -71,6 +77,10 @@ namespace MWBase = 0; virtual void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) = 0; virtual void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) = 0; + virtual void onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon, + const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth, + const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType) + = 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; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index deb9140320..157c12af23 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -526,9 +526,6 @@ namespace MWBase /// Spawn a random creature from a levelled list next to the player virtual void spawnRandomCreature(const ESM::RefId& creatureList) = 0; - /// Spawn a blood effect for \a ptr at \a worldPosition - virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; - virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true) = 0; diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 8bf9071f0c..37b0b85d45 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -12,6 +12,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwlua/localscripts.hpp" + #include "../mwworld/actionequip.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/containerstore.hpp" @@ -109,8 +111,23 @@ namespace MWClass return std::make_pair(slots_, false); } - ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const + ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const { + // We don't actually need an actor as such. We just need an object that has + // lua scripts and the Combat interface. + if (useLuaInterfaceIfAvailable) + { + // In this interface call, both objects are effectively const, so stripping Const from the ConstPtr is fine. + MWWorld::Ptr mutablePtr( + const_cast(ptr.mRef), const_cast(ptr.mCell)); + auto res = MWLua::LocalScripts::callPlayerInterface( + "Combat", "getArmorSkill", MWLua::LObject(mutablePtr)); + if (res) + return ESM::RefId::deserializeText(res.value()); + } + + // Fallback to the old engine implementation when actors don't have their scripts attached yet. + const MWWorld::LiveCellRef* ref = ptr.get(); std::string_view typeGmst; @@ -175,7 +192,7 @@ namespace MWClass const ESM::RefId& Armor::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - const ESM::RefId es = getEquipmentSkill(ptr); + const ESM::RefId es = getEquipmentSkill(ptr, false); static const ESM::RefId lightUp = ESM::RefId::stringRefId("Item Armor Light Up"); static const ESM::RefId mediumUp = ESM::RefId::stringRefId("Item Armor Medium Up"); static const ESM::RefId heavyUp = ESM::RefId::stringRefId("Item Armor Heavy Up"); @@ -190,7 +207,7 @@ namespace MWClass const ESM::RefId& Armor::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - const ESM::RefId es = getEquipmentSkill(ptr); + const ESM::RefId es = getEquipmentSkill(ptr, false); static const ESM::RefId lightDown = ESM::RefId::stringRefId("Item Armor Light Down"); static const ESM::RefId mediumDown = ESM::RefId::stringRefId("Item Armor Medium Down"); static const ESM::RefId heavyDown = ESM::RefId::stringRefId("Item Armor Heavy Down"); @@ -221,24 +238,29 @@ namespace MWClass std::string text; // get armor type string (light/medium/heavy) - std::string_view typeText; + std::string typeText; if (ref->mBase->mData.mWeight == 0) { // no type } else { - const ESM::RefId armorType = getEquipmentSkill(ptr); + const ESM::RefId armorType = getEquipmentSkill(ptr, true); if (armorType == ESM::Skill::LightArmor) typeText = "#{sLight}"; else if (armorType == ESM::Skill::MediumArmor) typeText = "#{sMedium}"; - else + else if (armorType == ESM::Skill::HeavyArmor) typeText = "#{sHeavy}"; + // For other skills, just subtitute the skill name + // Normally you would never see this case, but modding allows getEquipmentSkill() to return any skill. + else + typeText = "#{sSkill" + armorType.toString() + "}"; } text += "\n#{sArmorRating}: " - + MWGui::ToolTips::toString(static_cast(getEffectiveArmorRating(ptr, MWMechanics::getPlayer()))); + + MWGui::ToolTips::toString( + static_cast(getSkillAdjustedArmorRating(ptr, MWMechanics::getPlayer(), true))); int remainingHealth = getItemHealth(ptr); text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/" @@ -289,11 +311,25 @@ namespace MWClass return record->mId; } - float Armor::getEffectiveArmorRating(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& actor) const + float Armor::getSkillAdjustedArmorRating( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable) const { + if (useLuaInterfaceIfAvailable && actor == MWMechanics::getPlayer()) + { + // In this interface call, both objects are effectively const, so stripping Const from the ConstPtr is fine. + MWWorld::Ptr mutablePtr( + const_cast(ptr.mRef), const_cast(ptr.mCell)); + auto res = MWLua::LocalScripts::callPlayerInterface( + "Combat", "getSkillAdjustedArmorRating", MWLua::LObject(mutablePtr), MWLua::LObject(actor)); + if (res) + return res.value(); + } + + // Fallback to the old engine implementation when actors don't have their scripts attached yet. + const MWWorld::LiveCellRef* ref = ptr.get(); - const ESM::RefId armorSkillType = getEquipmentSkill(ptr); + const ESM::RefId armorSkillType = getEquipmentSkill(ptr, useLuaInterfaceIfAvailable); float armorSkill = actor.getClass().getSkill(actor, armorSkillType); int iBaseArmorSkill = MWBase::Environment::get() diff --git a/apps/openmw/mwclass/armor.hpp b/apps/openmw/mwclass/armor.hpp index 808bc078f4..e68dfe227f 100644 --- a/apps/openmw/mwclass/armor.hpp +++ b/apps/openmw/mwclass/armor.hpp @@ -41,7 +41,7 @@ namespace MWClass ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? - ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; + ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override; MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. @@ -81,7 +81,8 @@ namespace MWClass bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; /// Get the effective armor rating, factoring in the actor's skills, for the given armor. - float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const override; + float getSkillAdjustedArmorRating( + const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable) const override; }; } diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index 87d34c56d6..e303635309 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -98,7 +98,7 @@ namespace MWClass return std::make_pair(slots_, false); } - ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const + ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const { const MWWorld::LiveCellRef* ref = ptr.get(); diff --git a/apps/openmw/mwclass/clothing.hpp b/apps/openmw/mwclass/clothing.hpp index f95559f9c0..63764695b3 100644 --- a/apps/openmw/mwclass/clothing.hpp +++ b/apps/openmw/mwclass/clothing.hpp @@ -33,7 +33,7 @@ namespace MWClass ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? - ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; + ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override; MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index a1c632dab9..93052567fa 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -25,11 +25,14 @@ #include "../mwmechanics/setbaseaisetting.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwlua/localscripts.hpp" + #include "../mwworld/actionopen.hpp" #include "../mwworld/actiontalk.hpp" #include "../mwworld/cellstore.hpp" @@ -283,8 +286,8 @@ namespace MWClass if (!success) { - victim.getClass().onHit( - victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); + MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength, + 0.0f, false, hitPosition, false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -342,12 +345,12 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - victim.getClass().onHit( - victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); + MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength, + damage, healthdmg, hitPosition, true, MWMechanics::DamageSourceType::Melee); } - void Creature::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + void Creature::onHit(const MWWorld::Ptr& ptr, const std::map& damages, + const MWWorld::Ptr& object, const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); @@ -397,19 +400,44 @@ namespace MWClass if (!successful) { // Missed - if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) - MWBase::Environment::get().getSoundManager()->playSound3D( - ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f); return; } if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); - if (damage < 0.001f) - damage = 0; + bool hasDamage = false; + bool hasHealthDamage = false; + float healthDamage = 0.f; + for (auto& [stat, damage] : damages) + { + if (damage < 0.001f) + continue; + hasDamage = true; - if (damage > 0.f) + if (stat == "health") + { + hasHealthDamage = true; + healthDamage = damage; + MWMechanics::DynamicStat health(getCreatureStats(ptr).getHealth()); + health.setCurrent(health.getCurrent() - damage); + stats.setHealth(health); + } + else if (stat == "fatigue") + { + MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); + fatigue.setCurrent(fatigue.getCurrent() - damage, true); + stats.setFatigue(fatigue); + } + else if (stat == "magicka") + { + MWMechanics::DynamicStat magicka(getCreatureStats(ptr).getMagicka()); + magicka.setCurrent(magicka.getCurrent() - damage); + stats.setMagicka(magicka); + } + } + + if (hasDamage) { if (!attacker.isEmpty()) { @@ -420,35 +448,11 @@ namespace MWClass * getGmst().iKnockDownOddsMult->mValue.getInteger() * 0.01f + getGmst().iKnockDownOddsBase->mValue.getInteger(); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) + if (hasHealthDamage && agilityTerm <= healthDamage && knockdownTerm <= Misc::Rng::roll0to99(prng)) stats.setKnockedDown(true); else stats.setHitRecovery(true); // Is this supposed to always occur? } - - if (ishealth) - { - damage *= damage / (damage + getArmorRating(ptr)); - damage = std::max(1.f, damage); - if (!attacker.isEmpty()) - { - damage = scaleDamage(damage, attacker, ptr); - MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); - } - - MWBase::Environment::get().getSoundManager()->playSound3D( - ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); - - MWMechanics::DynamicStat health(stats.getHealth()); - health.setCurrent(health.getCurrent() - damage); - stats.setHealth(health); - } - else - { - MWMechanics::DynamicStat fatigue(stats.getFatigue()); - fatigue.setCurrent(fatigue.getCurrent() - damage, true); - stats.setFatigue(fatigue); - } } } @@ -591,7 +595,7 @@ namespace MWClass return info; } - float Creature::getArmorRating(const MWWorld::Ptr& ptr) const + float Creature::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const { // Equipment armor rating is deliberately ignored. return getCreatureStats(ptr).getMagicEffects().getOrDefault(ESM::MagicEffect::Shield).getMagnitude(); @@ -764,11 +768,6 @@ namespace MWClass } } - int Creature::getBloodTexture(const MWWorld::ConstPtr& ptr) const - { - return ptr.get()->mBase->mBloodType; - } - void Creature::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index b8619128c2..d7bb63011d 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -66,8 +66,8 @@ namespace MWClass void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, const osg::Vec3f& hitPosition, bool success) const override; - void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + void onHit(const MWWorld::Ptr& ptr, const std::map& damages, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const override; std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; @@ -88,7 +88,7 @@ namespace MWClass ///< Return total weight that fits into the object. Throws an exception, if the object can't /// hold other objects. - float getArmorRating(const MWWorld::Ptr& ptr) const override; + float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const override; ///< @return combined armor rating of this actor bool isEssential(const MWWorld::ConstPtr& ptr) const override; @@ -118,9 +118,6 @@ namespace MWClass float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; - /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) - int getBloodTexture(const MWWorld::ConstPtr& ptr) const override; - void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; ///< Read additional state from \a state into \a ptr. diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 693caeb5ae..da0e78bcd3 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -29,6 +29,8 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwlua/localscripts.hpp" + #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/aisetting.hpp" #include "../mwmechanics/autocalcspell.hpp" @@ -620,8 +622,8 @@ namespace MWClass float damage = 0.0f; if (!success) { - othercls.onHit( - victim, damage, false, weapon, ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); + MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength, + damage, false, hitPosition, false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); return; @@ -694,14 +696,13 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); + MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength, + damage, healthdmg, hitPosition, true, MWMechanics::DamageSourceType::Melee); } - void Npc::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, - const MWMechanics::DamageSourceType sourceType) const + void Npc::onHit(const MWWorld::Ptr& ptr, const std::map& damages, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const { - MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool wasDead = stats.isDead(); @@ -748,23 +749,47 @@ namespace MWClass if (!successful) { // Missed - if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f); return; } if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); - if (damage < 0.001f) - damage = 0; + if (ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState()) + return; - bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); + bool hasDamage = false; + bool hasHealthDamage = false; + float healthDamage = 0.f; + for (auto& [stat, damage] : damages) + { + if (damage < 0.001f) + continue; + hasDamage = true; - if (godmode) - damage = 0; + if (stat == "health") + { + hasHealthDamage = true; + healthDamage = damage; + MWMechanics::DynamicStat health(getCreatureStats(ptr).getHealth()); + health.setCurrent(health.getCurrent() - damage); + stats.setHealth(health); + } + else if (stat == "fatigue") + { + MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); + fatigue.setCurrent(fatigue.getCurrent() - damage, true); + stats.setFatigue(fatigue); + } + else if (stat == "magicka") + { + MWMechanics::DynamicStat magicka(getCreatureStats(ptr).getMagicka()); + magicka.setCurrent(magicka.getCurrent() - damage); + stats.setMagicka(magicka); + } + } - if (damage > 0.0f && !attacker.isEmpty()) + if (hasDamage && !attacker.isEmpty()) { // 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying // something, alert the character controller, scripts, etc. @@ -783,109 +808,16 @@ namespace MWClass float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f + gmst.iKnockDownOddsBase->mValue.getInteger(); - if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) + if (hasHealthDamage && agilityTerm <= healthDamage && knockdownTerm <= Misc::Rng::roll0to99(prng)) stats.setKnockedDown(true); else stats.setHitRecovery(true); // Is this supposed to always occur? - - if (damage > 0 && ishealth) - { - // Hit percentages: - // cuirass = 30% - // shield, helmet, greaves, boots, pauldrons = 10% each - // guantlets = 5% each - static const int hitslots[20] - = { MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_CarriedLeft, - MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Helmet, - MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_Boots, - MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_LeftPauldron, - MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_RightPauldron, - MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet }; - int hitslot = hitslots[Misc::Rng::rollDice(20, prng)]; - - float unmitigatedDamage = damage; - float x = damage / (damage + getArmorRating(ptr)); - damage *= std::max(gmst.fCombatArmorMinMult->mValue.getFloat(), x); - int damageDiff = static_cast(unmitigatedDamage - damage); - damage = std::max(1.f, damage); - damageDiff = std::max(1, damageDiff); - - MWWorld::InventoryStore& inv = getInventoryStore(ptr); - MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); - MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); - bool hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; - // If there's no item in the carried left slot or if it is not a shield redistribute the hit. - if (!hasArmor && hitslot == MWWorld::InventoryStore::Slot_CarriedLeft) - { - if (Misc::Rng::rollDice(2, prng) == 0) - hitslot = MWWorld::InventoryStore::Slot_Cuirass; - else - hitslot = MWWorld::InventoryStore::Slot_LeftPauldron; - armorslot = inv.getSlot(hitslot); - if (armorslot != inv.end()) - { - armor = *armorslot; - hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; - } - } - if (hasArmor) - { - // Unarmed creature attacks don't affect armor condition unless it was - // explicitly requested. - if (!object.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc() - || Settings::game().mUnarmedCreatureAttacksDamageArmor) - { - int armorhealth = armor.getClass().getItemHealth(armor); - armorhealth -= std::min(damageDiff, armorhealth); - armor.getCellRef().setCharge(armorhealth); - - // Armor broken? unequip it - if (armorhealth == 0) - armor = *inv.unequipItem(armor); - } - - ESM::RefId skill = armor.getClass().getEquipmentSkill(armor); - if (ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, skill, ESM::Skill::Armor_HitByOpponent); - - 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); - } - else if (ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, ESM::Skill::Unarmored, ESM::Skill::Armor_HitByOpponent); - } } - if (ishealth) + if (hasHealthDamage && healthDamage > 0.0f) { - if (!attacker.isEmpty() && !godmode) - damage = scaleDamage(damage, attacker, ptr); - - if (damage > 0.0f) - { - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); - if (ptr == MWMechanics::getPlayer()) - MWBase::Environment::get().getWindowManager()->activateHitOverlay(); - if (!attacker.isEmpty()) - MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); - } - MWMechanics::DynamicStat health(getCreatureStats(ptr).getHealth()); - health.setCurrent(health.getCurrent() - damage); - stats.setHealth(health); - } - else - { - MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); - fatigue.setCurrent(fatigue.getCurrent() - damage, true); - stats.setFatigue(fatigue); + if (ptr == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->activateHitOverlay(); } if (!wasDead && getCreatureStats(ptr).isDead()) @@ -1136,8 +1068,17 @@ namespace MWClass MWBase::Environment::get().getLuaManager()->skillUse(ptr, skill, usageType, extraFactor); } - float Npc::getArmorRating(const MWWorld::Ptr& ptr) const + float Npc::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const { + if (useLuaInterfaceIfAvailable && ptr == MWMechanics::getPlayer()) + { + auto res = MWLua::LocalScripts::callPlayerInterface("Combat", "getArmorRating"); + if (res) + return res.value(); + } + + // Fallback to the old engine implementation when actors don't have their scripts attached yet. + const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); @@ -1159,7 +1100,7 @@ namespace MWClass } else { - ratings[i] = it->getClass().getEffectiveArmorRating(*it, ptr); + ratings[i] = it->getClass().getSkillAdjustedArmorRating(*it, ptr); // Take in account armor condition const bool hasHealth = it->getClass().hasItemHealth(*it); @@ -1308,11 +1249,6 @@ namespace MWClass return getNpcStats(ptr).getSkill(id).getModified(); } - int Npc::getBloodTexture(const MWWorld::ConstPtr& ptr) const - { - return ptr.get()->mBase->mBloodType; - } - void Npc::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 29ab459242..b038d47337 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -81,8 +81,8 @@ namespace MWClass void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, const osg::Vec3f& hitPosition, bool success) const override; - void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + void onHit(const MWWorld::Ptr& ptr, const std::map& damages, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const override; void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const override; @@ -112,7 +112,7 @@ namespace MWClass ///< Returns total weight of objects inside this object (including modifications from magic /// effects). Throws an exception, if the object can't hold other objects. - float getArmorRating(const MWWorld::Ptr& ptr) const override; + float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const override; ///< @return combined armor rating of this actor void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override; @@ -137,9 +137,6 @@ namespace MWClass float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; - /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) - int getBloodTexture(const MWWorld::ConstPtr& ptr) const override; - bool isNpc() const override { return true; } void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 3524446b26..bee68a52e5 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -105,7 +105,7 @@ namespace MWClass return std::make_pair(slots_, stack); } - ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const + ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const { const MWWorld::LiveCellRef* ref = ptr.get(); int type = ref->mBase->mData.mType; diff --git a/apps/openmw/mwclass/weapon.hpp b/apps/openmw/mwclass/weapon.hpp index 9e79532bc0..96a2a0aa47 100644 --- a/apps/openmw/mwclass/weapon.hpp +++ b/apps/openmw/mwclass/weapon.hpp @@ -42,7 +42,7 @@ namespace MWClass ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? - ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; + ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override; int getValue(const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 1b9f146284..da30fa86ff 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -470,11 +470,10 @@ namespace MWGui if (mPtr.isEmpty()) return; - mArmorRating->setCaptionWithReplacing( - "#{sArmor}: " + MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + auto rating = MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr, true))); + mArmorRating->setCaptionWithReplacing("#{sArmor}: " + rating); if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) - mArmorRating->setCaptionWithReplacing( - MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + mArmorRating->setCaptionWithReplacing(rating); } void InventoryWindow::updatePreviewSize() diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp index 8eb90b48fe..bdf71710af 100644 --- a/apps/openmw/mwlua/corebindings.cpp +++ b/apps/openmw/mwlua/corebindings.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -159,6 +160,8 @@ namespace MWLua }; } + api["getGameDifficulty"] = []() { return Settings::game().mDifficulty.get(); }; + sol::table readOnlyApi = LuaUtil::makeReadOnly(api); return context.setTypePackage(readOnlyApi, "openmw_core"); } diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index 146eff95ba..b3ec647d0e 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -10,6 +10,7 @@ #include #include "../mwbase/luamanager.hpp" +#include "../mwmechanics/actorutil.hpp" #include "object.hpp" @@ -91,6 +92,19 @@ namespace MWLua void applyStatsCache(); + // Calls a lua interface on the player's scripts. This call is only meant for use in updating UI elements. + template + static std::optional callPlayerInterface( + std::string_view interfaceName, std::string_view identifier, const Args&... args) + { + auto player = MWMechanics::getPlayer(); + auto scripts = player.getRefData().getLuaScripts(); + if (scripts) + return scripts->callInterface(interfaceName, identifier, args...); + + return std::nullopt; + } + protected: SelfObject mData; diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 2fd7618ad7..9c2778e55d 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -5,8 +5,6 @@ #include #include -#include "sol/state_view.hpp" - #include #include @@ -152,6 +150,17 @@ namespace MWLua }); } + void LuaManager::sendLocalEvent( + const MWWorld::Ptr& target, const std::string& name, const std::optional& data) + { + LuaUtil::BinaryData binary = {}; + if (data) + { + binary = LuaUtil::serialize(*data, mLocalSerializer.get()); + } + mLuaEvents.addLocalEvent({ getId(target), name, binary }); + } + void LuaManager::update() { if (const int steps = Settings::lua().mGcStepsPerFrame; steps > 0) @@ -482,6 +491,49 @@ namespace MWLua EngineEvents::OnSkillLevelUp{ getId(actor), skillId.serializeText(), std::string(source) }); } + void LuaManager::onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon, + const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth, + const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType sourceType) + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + sol::table damageTable = view.newTable(); + if (isHealth) + damageTable["health"] = damage; + else + damageTable["fatigue"] = damage; + + sol::table data = view.newTable(); + if (!attacker.isEmpty()) + data["attacker"] = LObject(attacker); + if (!weapon.isEmpty()) + data["weapon"] = LObject(weapon); + if (!ammo.isEmpty()) + data["ammo"] = LObject(weapon); + data["type"] = attackType; + data["strength"] = attackStrength; + data["damage"] = damageTable; + data["hitPos"] = hitPos; + data["successful"] = successful; + switch (sourceType) + { + case MWMechanics::DamageSourceType::Unspecified: + data["sourceType"] = "unspecified"; + break; + case MWMechanics::DamageSourceType::Melee: + data["sourceType"] = "melee"; + break; + case MWMechanics::DamageSourceType::Ranged: + data["sourceType"] = "ranged"; + break; + case MWMechanics::DamageSourceType::Magical: + data["sourceType"] = "magic"; + break; + } + + sendLocalEvent(victim, "Hit", data); + }); + } + 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 9877c98fb9..f47b6f96cf 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -92,6 +92,9 @@ namespace MWLua bool loopfallback) override; void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) override; void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) override; + void onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon, + const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth, + const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType sourceType) override; void exteriorCreated(MWWorld::CellStore& cell) override { mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); @@ -166,6 +169,9 @@ namespace MWLua LuaUtil::InputAction::Registry& inputActions() { return mInputActions; } LuaUtil::InputTrigger::Registry& inputTriggers() { return mInputTriggers; } + void sendLocalEvent( + const MWWorld::Ptr& target, const std::string& name, const std::optional& data = std::nullopt); + private: void initConfiguration(); LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, diff --git a/apps/openmw/mwlua/types/actor.cpp b/apps/openmw/mwlua/types/actor.cpp index 413a656e90..1629235fdd 100644 --- a/apps/openmw/mwlua/types/actor.cpp +++ b/apps/openmw/mwlua/types/actor.cpp @@ -420,6 +420,42 @@ namespace MWLua return ptr.getClass().getCapacity(ptr); }; + actor["_onHit"] = [context](const SelfObject& self, const sol::table& options) { + sol::optional damageLua = options.get>("damage"); + std::map damageCpp; + if (damageLua) + { + for (auto& [key, value] : damageLua.value()) + { + damageCpp[key.as()] = value.as(); + } + } + std::string sourceTypeStr = options.get_or("sourceType", "unspecified"); + MWMechanics::DamageSourceType sourceType = MWMechanics::DamageSourceType::Unspecified; + if (sourceTypeStr == "melee") + sourceType = MWMechanics::DamageSourceType::Melee; + else if (sourceTypeStr == "ranged") + sourceType = MWMechanics::DamageSourceType::Ranged; + else if (sourceTypeStr == "magic") + sourceType = MWMechanics::DamageSourceType::Magical; + sol::optional weapon = options.get>("weapon"); + sol::optional ammo = options.get>("ammo"); + + context.mLuaManager->addAction( + [self = self, damages = std::move(damageCpp), attacker = options.get>("attacker"), + weapon = ammo ? ammo : weapon, successful = options.get("successful"), + sourceType = sourceType] { + MWWorld::Ptr attackerPtr; + MWWorld::Ptr weaponPtr; + if (attacker) + attackerPtr = attacker->ptr(); + if (weapon) + weaponPtr = weapon->ptr(); + self.ptr().getClass().onHit(self.ptr(), damages, weaponPtr, attackerPtr, successful, sourceType); + }, + "HitAction"); + }; + addActorStatsBindings(actor, context); addActorMagicBindings(actor, context); } diff --git a/apps/openmw/mwlua/types/creature.cpp b/apps/openmw/mwlua/types/creature.cpp index 4ebc658eb9..a9a1be9eee 100644 --- a/apps/openmw/mwlua/types/creature.cpp +++ b/apps/openmw/mwlua/types/creature.cpp @@ -74,6 +74,7 @@ namespace MWLua [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Essential; }); record["isRespawning"] = sol::readonly_property( [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Respawn; }); + record["bloodType"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mBloodType; }); addActorServicesBindings(record, context); } diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index e649c56a0f..380a2d1e9b 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -102,6 +102,7 @@ namespace MWLua record["isRespawning"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; }); record["baseGold"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mNpdt.mGold; }); + record["bloodType"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mBloodType; }); addActorServicesBindings(record, context); npc["classes"] = initClassRecordBindings(context); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index e7c7342284..7c0c674986 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -12,6 +12,7 @@ #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -240,8 +241,8 @@ namespace MWMechanics if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue)) { - victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false, - MWMechanics::DamageSourceType::Ranged); + MWBase::Environment::get().getLuaManager()->onHit(attacker, victim, weapon, projectile, 0, + attackStrength, damage, false, hitPosition, false, MWMechanics::DamageSourceType::Ranged); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); return; } @@ -299,8 +300,8 @@ namespace MWMechanics victim.getClass().getContainerStore(victim).add(projectile, 1); } - victim.getClass().onHit( - victim, damage, true, projectile, attacker, hitPosition, true, MWMechanics::DamageSourceType::Ranged); + MWBase::Environment::get().getLuaManager()->onHit(attacker, victim, weapon, projectile, 0, attackStrength, + damage, true, hitPosition, true, MWMechanics::DamageSourceType::Ranged); } } diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 92494f4f6e..efeb45dd86 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -359,8 +359,7 @@ namespace // Notify the target actor they've been hit bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) - target.getClass().onHit( - target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true, MWMechanics::DamageSourceType::Magical); + target.getClass().onHit(target, {}, MWWorld::Ptr(), caster, true, MWMechanics::DamageSourceType::Magical); // Apply resistances if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) { diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 105fbca80a..fc862f302f 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -119,8 +119,8 @@ namespace MWWorld throw std::runtime_error("class cannot hit"); } - 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 + void Class::onHit(const Ptr& ptr, const std::map& damages, const Ptr& object, + const Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const { throw std::runtime_error("class cannot be hit"); } @@ -205,7 +205,7 @@ namespace MWWorld return std::make_pair(std::vector(), false); } - ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr) const + ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const { return {}; } @@ -235,7 +235,7 @@ namespace MWWorld return false; } - float Class::getArmorRating(const MWWorld::Ptr& ptr) const + float Class::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const { throw std::runtime_error("Class does not support armor rating"); } @@ -452,11 +452,6 @@ namespace MWWorld throw std::runtime_error("class does not support skills"); } - int Class::getBloodTexture(const MWWorld::ConstPtr& ptr) const - { - throw std::runtime_error("class does not support gore"); - } - void Class::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const {} void Class::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const {} @@ -514,7 +509,8 @@ namespace MWWorld return -1; } - float Class::getEffectiveArmorRating(const ConstPtr& armor, const Ptr& actor) const + float Class::getSkillAdjustedArmorRating( + const ConstPtr& armor, const Ptr& actor, bool useLuaInterfaceIfAvailable) const { throw std::runtime_error("class does not support armor ratings"); } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 7e49fad61b..5633c90b12 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -147,11 +147,11 @@ namespace MWWorld /// enums. ignored for creature attacks. /// (default implementation: throw an exception) - virtual void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + virtual void onHit(const MWWorld::Ptr& ptr, const std::map& damages, + const MWWorld::Ptr& object, const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const; - ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is - /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the + ///< Alerts \a ptr that it's being hit for \a damages by \a object (sword, arrow, etc). \a attacker specifies + ///< the /// actor responsible for the attack. \a successful specifies if the hit is /// successful or not. \a sourceType classifies the damage source. @@ -212,7 +212,7 @@ namespace MWWorld /// /// Default implementation: return (empty vector, false). - virtual ESM::RefId getEquipmentSkill(const ConstPtr& ptr) const; + virtual ESM::RefId getEquipmentSkill(const ConstPtr& ptr, bool useLuaInterfaceIfAvailable = false) const; /// Return the index of the skill this item corresponds to when equipped. /// (default implementation: return empty ref id) @@ -258,7 +258,7 @@ namespace MWWorld virtual ESM::RefId getSoundIdFromSndGen(const Ptr& ptr, std::string_view type) const; ///< Returns the sound ID for \a ptr of the given soundgen \a type. - virtual float getArmorRating(const MWWorld::Ptr& ptr) const; + virtual float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable = false) const; ///< @return combined armor rating of this actor virtual const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const; @@ -313,9 +313,6 @@ namespace MWWorld virtual bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const { return true; } ///< Return whether this class of object can be activated with telekinesis - /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) - virtual int getBloodTexture(const MWWorld::ConstPtr& ptr) const; - virtual Ptr copyToCell(const ConstPtr& ptr, CellStore& cell, int count) const; // Similar to `copyToCell`, but preserves RefNum and moves LuaScripts. @@ -378,7 +375,8 @@ namespace MWWorld virtual int getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const; /// Get the effective armor rating, factoring in the actor's skills, for the given armor. - virtual float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const; + virtual float getSkillAdjustedArmorRating( + const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable = false) const; virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const; diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 86af303341..ec3355b09e 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -408,7 +408,7 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) { if (actorIsNpc) { - if (testCls.getEffectiveArmorRating(test, actor) <= unarmoredRating) + if (testCls.getSkillAdjustedArmorRating(test, actor) <= unarmoredRating) continue; } else @@ -463,8 +463,8 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) // For NPCs, compare armor rating; for creatures, compare condition if (actorIsNpc) { - const float rating = testCls.getEffectiveArmorRating(test, actor); - const float oldRating = oldCls.getEffectiveArmorRating(old, actor); + const float rating = testCls.getSkillAdjustedArmorRating(test, actor); + const float oldRating = oldCls.getSkillAdjustedArmorRating(old, actor); if (rating <= oldRating) continue; } diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index e0b62c94b6..6c4912d867 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -105,7 +105,7 @@ namespace MWWorld void setLocals(const ESM::Script& script); - MWLua::LocalScripts* getLuaScripts() { return mLuaScripts.get(); } + MWLua::LocalScripts* getLuaScripts() const { return mLuaScripts.get(); } void setLuaScripts(std::shared_ptr&&); /// This flag is only used for content stack loading and will not be stored in the savegame. diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 7c0d6f4df8..c07f5b9161 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3706,24 +3706,6 @@ namespace MWWorld } } - void World::spawnBloodEffect(const Ptr& ptr, const osg::Vec3f& worldPosition) - { - if (ptr == getPlayerPtr() && Settings::gui().mHitFader) - return; - - std::string_view texture - = Fallback::Map::getString("Blood_Texture_" + std::to_string(ptr.getClass().getBloodTexture(ptr))); - if (texture.empty()) - texture = Fallback::Map::getString("Blood_Texture_0"); - - // [0, 2] - const int number = Misc::Rng::rollDice(3); - const VFS::Path::Normalized model = Misc::ResourceHelpers::correctMeshPath( - VFS::Path::Normalized(Fallback::Map::getString("Blood_Model_" + std::to_string(number)))); - - mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false, false); - } - void World::spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale, bool isMagicVFX, bool useAmbientLight) { diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 4e93acee45..16f91177a1 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -608,9 +608,6 @@ namespace MWWorld /// Spawn a random creature from a levelled list next to the player void spawnRandomCreature(const ESM::RefId& creatureList) override; - /// Spawn a blood effect for \a ptr at \a worldPosition - void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override; - void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true) override; diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 956b4d4317..dd066d3414 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -165,6 +165,29 @@ namespace LuaUtil virtual bool isActive() const { return false; } protected: + // Call a function on an interface. + template + std::optional callInterface(std::string_view interfaceName, std::string_view identifier, const Args&... args) + { + std::optional res = std::nullopt; + mLua.protectedCall([&](LuaUtil::LuaView& view) { + LoadedData& data = ensureLoaded(); + auto I = data.mPublicInterfaces.get>(interfaceName); + if (I) + { + auto o = I->get_or(identifier, sol::nil); + if (o.is()) + { + sol::object luaRes = o.as().call(args...); + if (luaRes.is()) + res = luaRes.as(); + } + } + }); + + return res; + } + struct Handler { int mScriptId; diff --git a/components/settings/categories/game.hpp b/components/settings/categories/game.hpp index ec6f9dc206..ac5a9a08c3 100644 --- a/components/settings/categories/game.hpp +++ b/components/settings/categories/game.hpp @@ -71,8 +71,6 @@ namespace Settings SettingValue mDefaultActorPathfindHalfExtents{ mIndex, "Game", "default actor pathfind half extents", makeMaxStrictSanitizerVec3f(osg::Vec3f(0, 0, 0)) }; SettingValue mDayNightSwitches{ mIndex, "Game", "day night switches" }; - SettingValue mUnarmedCreatureAttacksDamageArmor{ mIndex, "Game", - "unarmed creature attacks damage armor" }; SettingValue mActorCollisionShapeType{ mIndex, "Game", "actor collision shape type" }; SettingValue mPlayerMovementIgnoresAnimation{ mIndex, "Game", "player movement ignores animation" }; diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 997d5846af..6ad2ec90cc 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/combat/local.lua scripts/omw/input/playercontrols.lua scripts/omw/mechanics/animationcontroller.lua scripts/omw/input/gamepadcontrols.lua diff --git a/docs/source/reference/lua-scripting/events.rst b/docs/source/reference/lua-scripting/events.rst index 15c9b52eea..590ff14d88 100644 --- a/docs/source/reference/lua-scripting/events.rst +++ b/docs/source/reference/lua-scripting/events.rst @@ -71,11 +71,70 @@ Calls the corresponding function in openw.core on the target. Will use core.soun .. code-block:: Lua actor:sendEvent('PlaySound3d', {sound = 'Open Lock'}) - **BreakInvisibility** Forces the actor to lose all active invisibility effects. +**Unequip** + +Any script can send ``Unequip`` events with the argument ``item`` or ``slot`` to any actor, to make that actor unequip an item. + +The following two examples are equivalent, except the ``item`` variant is guaranteed to only unequip the specified item and won't unequip a different item if the actor's equipment changed during the same frame: + +.. code-block:: Lua + + local item = Actor.getEquipment(actor, Actor.EQUIPMENT_SLOT.CarriedLeft) + if item then + actor:sendEvent('Unequip', {item = item}) + end + +.. code-block:: Lua + + actor:sendEvent('Unequip', {slot = Actor.EQUIPMENT_SLOT.CarriedLeft}) + +Combat events +------------- + +**Hit** + +Any script can send ``Hit`` events with arguments described in the Combat interface to cause a hit to an actor + +Example: + +.. code-block:: Lua + + -- See Combat#AttackInfo + local attack = { + attacker = self, + weapon = Actor.getEquipment(self, Actor.EQUIPMENT_SLOT.CarriedRight), + sourceType = I.Combat.ATTACK_SOURCE_TYPE.Melee, + strenght = 1, + type = self.ATTACK_TYPE.Chop, + damage = { + health = 20, + fatigue = 10, + }, + successful = true, + } + victim:sendEvent('Hit', attack) + +Item events +----------- + +**ModifyItemCondition** + +Any script can send ``ModifyItemCondition`` events to global to adjust the condition of an item. + +Example: + +.. code-block:: Lua + + local item = Actor.getEquipment(actor, Actor.EQUIPMENT_SLOT.CarriedLeft) + if item then + -- Reduce condition by 1 + -- Note that actor should be included, if applicable, to allow forcibly unequipping items whose condition is reduced to 0 + core:sendGlobalEvent('ModifyItemCondition', {actor = self, item = item, amount: -1}) + end UI events --------- diff --git a/docs/source/reference/lua-scripting/interface_combat.rst b/docs/source/reference/lua-scripting/interface_combat.rst new file mode 100644 index 0000000000..e3175ea27c --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_combat.rst @@ -0,0 +1,8 @@ +Interface Combat +================ + +.. include:: version.rst + +.. raw:: html + :file: generated_html/scripts_omw_combat_local.html + diff --git a/docs/source/reference/lua-scripting/tables/interfaces.rst b/docs/source/reference/lua-scripting/tables/interfaces.rst index 8496d01029..562666c85e 100644 --- a/docs/source/reference/lua-scripting/tables/interfaces.rst +++ b/docs/source/reference/lua-scripting/tables/interfaces.rst @@ -23,6 +23,9 @@ * - :doc:`Crimes ` - |bdg-ctx-global| - Commit crimes. + * - :doc:`Combat ` + - |bdg-ctx-local| + - Control combat of NPCs and creatures * - :doc:`GamepadControls ` - |bdg-ctx-player| - Allows to alter behavior of the built-in script that handles player gamepad controls. diff --git a/docs/source/reference/modding/settings/GUI.rst b/docs/source/reference/modding/settings/GUI.rst index 63bbae5f8b..7dc38f8c39 100644 --- a/docs/source/reference/modding/settings/GUI.rst +++ b/docs/source/reference/modding/settings/GUI.rst @@ -80,7 +80,6 @@ GUI Settings Enables or disables the red flash overlay when the character takes damage. - Disabling causes the player to "bleed" like NPCs. .. omw-setting:: :title: werewolf overlay diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 19d31279c6..fcbc8763ac 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -442,17 +442,6 @@ Game Settings 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. -.. omw-setting:: - :title: unarmed creature attacks damage armor - :type: boolean - :range: true, false - :default: false - :location: :bdg-success:`Launcher > Settings > Gameplay` - - If disabled unarmed creature attacks do not reduce armor condition, just as with vanilla engine. - - If enabled unarmed creature attacks reduce armor condition, the same as attacks from NPCs and armed creatures. - .. omw-setting:: :title: actor collision shape type :type: int diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 39715af75d..31ecb530ca 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -43,6 +43,9 @@ set(BUILTIN_DATA_FILES l10n/OMWCamera/ru.yaml l10n/OMWCamera/sv.yaml l10n/OMWCamera/fr.yaml + l10n/OMWCombat/en.yaml + l10n/OMWCombat/ru.yaml + l10n/OMWCombat/sv.yaml l10n/OMWControls/de.yaml l10n/OMWControls/en.yaml l10n/OMWControls/fr.yaml @@ -87,6 +90,10 @@ set(BUILTIN_DATA_FILES scripts/omw/camera/settings.lua scripts/omw/camera/move360.lua scripts/omw/camera/first_person_auto_switch.lua + scripts/omw/combat/common.lua + scripts/omw/combat/global.lua + scripts/omw/combat/local.lua + scripts/omw/combat/menu.lua scripts/omw/console/global.lua scripts/omw/console/local.lua scripts/omw/console/player.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 6126c0b761..0d0d2eb8a7 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -25,6 +25,9 @@ PLAYER: scripts/omw/input/gamepadcontrols.lua NPC,CREATURE: scripts/omw/ai.lua GLOBAL: scripts/omw/mechanics/globalcontroller.lua CREATURE, NPC, PLAYER: scripts/omw/mechanics/actorcontroller.lua +GLOBAL: scripts/omw/combat/global.lua +MENU: scripts/omw/combat/menu.lua +NPC,CREATURE,PLAYER: scripts/omw/combat/local.lua # User interface PLAYER: scripts/omw/ui.lua diff --git a/files/data/l10n/OMWCombat/en.yaml b/files/data/l10n/OMWCombat/en.yaml new file mode 100644 index 0000000000..b2331aefd3 --- /dev/null +++ b/files/data/l10n/OMWCombat/en.yaml @@ -0,0 +1,16 @@ +Combat: "OpenMW Combat" +combatSettingsPageDescription: "OpenMW Combat settings" + +combatSettings: "Combat" + +unarmedCreatureAttacksDamageArmor: "Unarmed creature attacks damage armor" +unarmedCreatureAttacksDamageArmorDescription: | + Unarmed creatures now also damage armor. + +redistributeShieldHitsWhenNotWearingShield: "Redistribute shield hits when not wearing a shield" +redistributeShieldHitsWhenNotWearingShieldDescription: | + Equivalent to "Shield hit location fix" from Morrowind Code Patch. Redistributes hits to the shield armor slot to left pauldron or cuirass when not wearing a shield. + +spawnBloodEffectsOnPlayer: "Spawn blood effects on player" +spawnBloodEffectsOnPlayerDescription: | + If enabled blood effects are spawned on the player when hit in combat, same as any other character. diff --git a/files/data/l10n/OMWCombat/ru.yaml b/files/data/l10n/OMWCombat/ru.yaml new file mode 100644 index 0000000000..8c616e90fc --- /dev/null +++ b/files/data/l10n/OMWCombat/ru.yaml @@ -0,0 +1,16 @@ +Combat: "Боевая система OpenMW" +combatSettingsPageDescription: "Настройки боевой системы OpenMW" + +combatSettings: "Бой" + +unarmedCreatureAttacksDamageArmor: "Атаки существ повреждают броню" +unarmedCreatureAttacksDamageArmorDescription: | + Атаки невооруженных существ тоже будут наносить броне урон. + +redistributeShieldHitsWhenNotWearingShield: "Перераспространять удары по щитам" +redistributeShieldHitsWhenNotWearingShieldDescription: | + Эквивалентно настройке "Shield hit location fix" из Morrowind Code Patch. Когда персонаж не носит щит, удары по его слоту будут проходить по слоту левого наплечника или кирасы. + +spawnBloodEffectsOnPlayer: "Использовать эффекты крови для игрока" +spawnBloodEffectsOnPlayerDescription: | + Если настройка включена, эффекты крови будут играть при получении ударов в бою персонажем игрока, как для всех остальных персонажей. diff --git a/files/data/l10n/OMWCombat/sv.yaml b/files/data/l10n/OMWCombat/sv.yaml new file mode 100644 index 0000000000..e8797c77bc --- /dev/null +++ b/files/data/l10n/OMWCombat/sv.yaml @@ -0,0 +1,16 @@ +Combat: "OpenMW Strid" +combatSettingsPageDescription: "OpenMW Stridsinställningar" + +combatSettings: "Strid" + +unarmedCreatureAttacksDamageArmor: "Obeväpnad attack från varelser skadar rustning" +unarmedCreatureAttacksDamageArmorDescription: | + Obeväpnade varelser skadar nu även rustning. + +redistributeShieldHitsWhenNotWearingShield: "Omfördela sköldträffar när sköld inte är buren" +redistributeShieldHitsWhenNotWearingShieldDescription: | + Motsvarar ”Shield hit location fix” från Morrowind Code Patch. Omfördelar träffar på sköldens rustningsplats till vänster axelstycke (pauldron) eller kyrass (cuirass) när du inte bär en sköld. + +spawnBloodEffectsOnPlayer: "Skapa blodeffekter på spelarfiguren" +spawnBloodEffectsOnPlayerDescription: | + Om blodeffekter är aktiverade visas de på spelarfiguren när denne träffas i strid, precis som på alla andra rollfigurer. diff --git a/files/data/scripts/omw/combat/common.lua b/files/data/scripts/omw/combat/common.lua new file mode 100644 index 0000000000..e2225a5741 --- /dev/null +++ b/files/data/scripts/omw/combat/common.lua @@ -0,0 +1,41 @@ +local async = require('openmw.async') +local storage = require('openmw.storage') +local I = require('openmw.interfaces') + +local combatGroup = 'SettingsOMWCombat' + +return { + registerSettingsPage = function() + I.Settings.registerPage({ + key = 'OMWCombat', + l10n = 'OMWCombat', + name = 'Combat', + description = 'combatSettingsPageDescription', + }) + end, + registerSettingsGroup = function() + local function boolSetting(key, default) + return { + key = key, + renderer = 'checkbox', + name = key, + description = key..'Description', + default = default, + } + end + + I.Settings.registerGroup({ + key = combatGroup, + page = 'OMWCombat', + l10n = 'OMWCombat', + name = 'combatSettings', + permanentStorage = false, + order = 0, + settings = { + boolSetting('unarmedCreatureAttacksDamageArmor', false), + boolSetting('redistributeShieldHitsWhenNotWearingShield', false), + boolSetting('spawnBloodEffectsOnPlayer', false), + }, + }) + end, +} diff --git a/files/data/scripts/omw/combat/global.lua b/files/data/scripts/omw/combat/global.lua new file mode 100644 index 0000000000..a1b0da7ed4 --- /dev/null +++ b/files/data/scripts/omw/combat/global.lua @@ -0,0 +1 @@ +require('scripts.omw.combat.common').registerSettingsGroup() diff --git a/files/data/scripts/omw/combat/local.lua b/files/data/scripts/omw/combat/local.lua new file mode 100644 index 0000000000..504f10a1fd --- /dev/null +++ b/files/data/scripts/omw/combat/local.lua @@ -0,0 +1,431 @@ +local animation = require('openmw.animation') +local async = require('openmw.async') +local core = require('openmw.core') +local I = require('openmw.interfaces') +local self = require('openmw.self') +local storage = require('openmw.storage') +local types = require('openmw.types') +local util = require('openmw.util') +local Actor = types.Actor +local Weapon = types.Weapon +local Player = types.Player +local Creature = types.Creature +local Armor = types.Armor +local isPlayer = Player.objectIsInstance(self) + +local godMode = function() return false end +if isPlayer then + -- openmw.debug is only allowed on player scripts + godMode = function() return require('openmw.debug').isGodMode() end +end + +local onHitHandlers = {} + +local settings = storage.globalSection('SettingsOMWCombat') + +local function getSkill(actor, skillId) + if Creature.objectIsInstance(actor) then + local specialization = core.stats.Skill.record(skillId).specialization + local creatureRecord = Creature.record(actor) + return creatureRecord[specialization..'Skill'] + else + return types.NPC.stats.skills[skillId](actor).modified + end +end + +local armorTypeGmst = { + [Armor.TYPE.Boots] = core.getGMST('iBootsWeight'), + [Armor.TYPE.Cuirass] = core.getGMST('iCuirassWeight'), + [Armor.TYPE.Greaves] = core.getGMST('iGreavesWeight'), + [Armor.TYPE.Helmet] = core.getGMST('iHelmWeight'), + [Armor.TYPE.LBracer] = core.getGMST('iGauntletWeight'), + [Armor.TYPE.LGauntlet] = core.getGMST('iGauntletWeight'), + [Armor.TYPE.LPauldron] = core.getGMST('iPauldronWeight'), + [Armor.TYPE.RBracer] = core.getGMST('iGauntletWeight'), + [Armor.TYPE.RGauntlet] = core.getGMST('iGauntletWeight'), + [Armor.TYPE.RPauldron] = core.getGMST('iPauldronWeight'), + [Armor.TYPE.Shield] = core.getGMST('iShieldWeight'), +} + +local armorSlots = { + Actor.EQUIPMENT_SLOT.Boots, + Actor.EQUIPMENT_SLOT.Cuirass, + Actor.EQUIPMENT_SLOT.Greaves, + Actor.EQUIPMENT_SLOT.Helmet, + Actor.EQUIPMENT_SLOT.LeftGauntlet, + Actor.EQUIPMENT_SLOT.LeftPauldron, + Actor.EQUIPMENT_SLOT.RightGauntlet, + Actor.EQUIPMENT_SLOT.RightPauldron, + Actor.EQUIPMENT_SLOT.CarriedLeft, +} + +local function getArmorSkill(item) + if not item or not Armor.objectIsInstance(item) then + return 'unarmored' + end + local record = Armor.record(item) + local weightGmst = armorTypeGmst[record.type] + local epsilon = 0.0005 + if record.weight <= weightGmst * core.getGMST('fLightMaxMod') + epsilon then + return 'lightarmor' + elseif record.weight <= weightGmst * core.getGMST('fMedMaxMod') + epsilon then + return 'mediumarmor' + else + return 'heavyarmor' + end +end + +local function getSkillAdjustedArmorRating(item, actor) + local record = Armor.record(item) + local skillid = I.Combat.getArmorSkill(item) + local skill = getSkill(actor, skillid) + if record.weight == 0 then + return record.baseArmor + end + return record.baseArmor * skill / core.getGMST('iBaseArmorSkill') +end + +local function getEffectiveArmorRating(item, actor) + local record = Armor.record(item) + local rating = getSkillAdjustedArmorRating(item, actor) + if record.health and record.health ~= 0 then + rating = rating * (types.Item.itemData(item).condition / record.health) + end + return rating +end + +local function getArmorRating(actor) + local magicShield = Actor.activeEffects(actor):getEffect(core.magic.EFFECT_TYPE.Shield).magnitude + + if Creature.objectIsInstance(actor) then + return magicShield + end + + local equipment = Actor.getEquipment(actor) + local ratings = {} + local unarmored = getSkill(actor, 'unarmored') + local fUnarmoredBase1 = core.getGMST('fUnarmoredBase1') + local fUnarmoredBase2 = core.getGMST('fUnarmoredBase2') + + for _, v in pairs(armorSlots) do + if equipment[v] and Armor.objectIsInstance(equipment[v]) then + ratings[v] = I.Combat.getEffectiveArmorRating(equipment[v], actor) + else + -- Unarmored + ratings[v] = (fUnarmoredBase1 * unarmored) * (fUnarmoredBase2 * unarmored) + end + end + + return ratings[Actor.EQUIPMENT_SLOT.Cuirass] * 0.3 + + ratings[Actor.EQUIPMENT_SLOT.CarriedLeft] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.Helmet] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.Greaves] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.Boots] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.LeftPauldron] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.RightPauldron] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.LeftGauntlet] * 0.05 + + ratings[Actor.EQUIPMENT_SLOT.RightGauntlet] * 0.05 + + magicShield +end + +local function adjustDamageForArmor(damage, actor) + local armor = I.Combat.getArmorRating(actor) + local x = damage / (damage + armor) + return damage * math.max(x, core.getGMST('fCombatArmorMinMult')) +end + +local function pickRandomArmor(actor) + local slot = nil + local roll = math.random(0, 99) -- randIntUniform(0, 100) + if roll >= 90 then + slot = Actor.EQUIPMENT_SLOT.CarriedLeft + local item = Actor.getEquipment(actor, slot) + local haveShield = item and Armor.objectIsInstance(item) + if settings:get('redistributeShieldHitsWhenNotWearingShield') and not haveShield then + if roll >= 95 then + slot = Actor.EQUIPMENT_SLOT.Cuirass + else + slot = Actor.EQUIPMENT_SLOT.LeftPauldron + end + end + elseif roll >= 85 then + slot = Actor.EQUIPMENT_SLOT.RightGauntlet + elseif roll >= 80 then + slot = Actor.EQUIPMENT_SLOT.LeftGauntlet + elseif roll >= 70 then + slot = Actor.EQUIPMENT_SLOT.RightPauldron + elseif roll >= 60 then + slot = Actor.EQUIPMENT_SLOT.LeftPauldron + elseif roll >= 50 then + slot = Actor.EQUIPMENT_SLOT.Boots + elseif roll >= 40 then + slot = Actor.EQUIPMENT_SLOT.Greaves + elseif roll >= 30 then + slot = Actor.EQUIPMENT_SLOT.Helmet + else + slot = Actor.EQUIPMENT_SLOT.Cuirass + end + + return Actor.getEquipment(actor, slot) +end + +local function getDamage(attack, what) + if attack.damage then + return attack.damage[what] or 0 + end +end + +local function setDamage(attack, what, damage) + attack.damage = attack.damage or {} + attack.damage[what] = damage +end + +local function applyArmor(attack) + local healthDamage = getDamage(attack, 'health') + if healthDamage > 0 then + local healthDamageAdjusted = I.Combat.adjustDamageForArmor(healthDamage) + local diff = math.floor(healthDamageAdjusted - healthDamage) + setDamage(attack, 'health', math.max(healthDamageAdjusted, 1)) + local item = I.Combat.pickRandomArmor() + local skillid = I.Combat.getArmorSkill(item) + if I.SkillProgression then + I.SkillProgression.skillUsed(skillid, {useType = I.SkillProgression.SKILL_USE_TYPES.Armor_HitByOpponent}) + end + if item and Armor.objectIsInstance(item) then + local attackerIsUnarmedCreature = attack.attacker and not attack.weapon and Creature.objectIsInstance(attack.attacker) + if settings:get('unarmedCreatureAttacksDamageArmor') or not attackerIsUnarmedCreature then + core.sendGlobalEvent('ModifyItemCondition', { actor = self, item = item, amount = diff }) + end + + if skillid == 'lightarmor' then + core.sound.playSound3d('Light Armor Hit', self) + elseif skillid == 'mediumarmor' then + core.sound.playSound3d('Medium Armor Hit', self) + elseif skillid == 'heavyarmor' then + core.sound.playSound3d('Heavy Armor Hit', self) + else + core.sound.playSound3d('Hand To Hand Hit', self) + end + end + end +end + +local function adjustDamageForDifficulty(attack, defendant) + local attackerIsPlayer = attack.attacker and Player.objectIsInstance(attack.attacker) + -- The interface guarantees defendant is never nil + local defendantIsPlayer = Player.objectIsInstance(defendant) + -- If both characters are NPCs or both characters are players then + -- difficulty settings do not apply + if attackerIsPlayer == defendantIsPlayer then return end + + local fDifficultyMult = core.getGMST('fDifficultyMult') + local difficultyTerm = core.getGameDifficulty() * 0.01 + local x = 0 + + if defendantIsPlayer then + -- Defending actor is a player + if difficultyTerm > 0 then + x = difficultyTerm * fDifficultyMult + else + x = difficultyTerm / fDifficultyMult + end + elseif attackerIsPlayer then + -- Attacking actor is a player + if difficultyTerm > 0 then + x = -difficultyTerm / fDifficultyMult + else + x = -difficultyTerm * fDifficultyMult + end + end + + setDamage(attack, 'health', getDamage(attack, 'health') * (1 + x)) +end + +local function spawnBloodEffect(position) + if isPlayer and not settings:get('spawnBloodEffectsOnPlayer') then + return + end + + local bloodEffectModel = string.format('Blood_Model_%d', math.random(0, 2)) -- randIntUniformClosed(0, 2) + + -- TODO: implement a Misc::correctMeshPath equivalent instead? + -- All it ever does it append 'meshes\\' though + bloodEffectModel = 'meshes/'..core.getGMST(bloodEffectModel) + + local record = self.object.type.record(self.object) + local bloodTexture = string.format('Blood_Texture_%d', record.bloodType) + bloodTexture = core.getGMST(bloodTexture) + if not bloodTexture or bloodTexture == '' then + bloodTexture = core.getGMST('Blood_Texture_0') + end + core.sendGlobalEvent('SpawnVfx', { + model = bloodEffectModel, + position = position, + options = { + mwMagicVfx = false, + particleTextureOverride = bloodTexture, + useAmbientLight = false, + }, + }) +end + +local function onHit(data) + for i = #onHitHandlers, 1, -1 do + if onHitHandlers[i](data) == false then + return -- skip other handlers + end + end + if data.successful and not godMode() then + I.Combat.applyArmor(data) + I.Combat.adjustDamageForDifficulty(data) + if getDamage(data, 'health') > 0 then + core.sound.playSound3d('Health Damage', self) + if data.hitPos then + spawnBloodEffect(data.hitPos) + end + end + elseif data.attacker and Player.objectIsInstance(data.attacker) then + core.sound.playSound3d('miss', self) + end + Actor._onHit(self, data) +end + +--- +-- Table of possible attack source types +-- @type AttackSourceType +-- @field #string Magic +-- @field #string Melee +-- @field #string Ranged +-- @field #string Unspecified + +--- +-- @type AttackInfo +-- @field [parent=#AttackInfo] #table damage A table mapping stat name (health, fatigue, or magicka) to number. For example, {health = 50, fatigue = 10} will cause 50 damage to health and 10 to fatigue (before adjusting for armor and difficulty). This field is ignored for failed attacks. +-- @field [parent=#AttackInfo] #number strength A number between 0 and 1 representing the attack strength. This field is ignored for failed attacks. +-- @field [parent=#AttackInfo] #boolean successful Whether the attack was successful or not. +-- @field [parent=#AttackInfo] #AttackSourceType sourceType What class of attack this is. +-- @field [parent=#AttackInfo] openmw.self#ATTACK_TYPE type (Optional) Attack variant if applicable. For melee attacks this represents chop vs thrust vs slash. For unarmed creatures this implies which of its 3 possible attacks were used. For other attacks this field can be ignored. +-- @field [parent=#AttackInfo] openmw.types#Actor attacker (Optional) Attacking actor +-- @field [parent=#AttackInfo] openmw.types#Weapon weapon (Optional) Attacking weapon +-- @field [parent=#AttackInfo] openmw.types#Weapon ammo (Optional) Ammo +-- @field [parent=#AttackInfo] openmw.util#Vector3 hitPos (Optional) Where on the victim the attack is landing. Used to spawn blood effects. Blood effects are skipped if nil. +return { + --- Basic combat interface + -- @module Combat + -- @usage require('openmw.interfaces').Combat + -- + --I.Combat.addOnHitHandler(function(attack) + -- -- Adds fatigue loss when hit by draining fatigue when taking health damage + -- if attack.damage.health and not attack.damage.fatigue then + -- local strengthFactor = Actor.stats.attributes.strength(self).modified / 100 * 0.66 + -- local enduranceFactor = Actor.stats.attributes.endurance(self).modified / 100 * 0.34 + -- local factor = 1 - math.min(strengthFactor + enduranceFactor, 1) + -- if factor > 0 then + -- attack.damage.fatigue = attack.damage.health * factor + -- end + -- end + --end) + + interfaceName = 'Combat', + interface = { + --- Interface version + -- @field [parent=#Combat] #number version + version = 0, + + --- Add new onHit handler for this actor + -- If `handler(attack)` returns false, other handlers for + -- the call will be skipped. where attack is the same @{#AttackInfo} passed to #Combat.onHit + -- @function [parent=#Combat] addOnHitHandler + -- @param #function handler The handler. + addOnHitHandler = function(handler) + onHitHandlers[#onHitHandlers + 1] = handler + end, + + --- Calculates the character's armor rating and adjusts damage accordingly. + -- Note that this function only adjusts the number, use #Combat.applyArmor + -- to include other side effects. + -- @function [parent=#Combat] adjustDamageForArmor + -- @param #number Damage The numeric damage to adjust + -- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self. + -- @return #number Damage adjusted for armor + adjustDamageForArmor = function(damage, actor) return adjustDamageForArmor(damage, actor or self) end, + + --- Calculates a difficulty multiplier based on current difficulty settings + -- and adjusts damage accordingly. Has no effect if both this actor and the + -- attacker are NPCs, or if both are Players. + -- @function [parent=#Combat] adjustDamageForDifficulty + -- @param #Attack attack The attack to adjust + -- @param openmw.core#GameObject defendant (Optional) The defendant to make the difficulty adjustment for. Defaults to self. + adjustDamageForDifficulty = function(attack, defendant) return adjustDamageForDifficulty(attack, defendant or self) end, + + --- Applies this character's armor to the attack. Adjusts damage, reduces item + -- condition accordingly, progresses armor skill, and plays the armor appropriate + -- hit sound. + -- @function [parent=#Combat] applyArmor + -- @param #Attack attack + applyArmor = applyArmor, + + --- Computes this character's armor rating. + -- Note that this interface function is read by the engine to update the UI. + -- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything. + -- @function [parent=#Combat] getArmorRating + -- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self. + -- @return #number + getArmorRating = function(actor) return getArmorRating(actor or self) end, + + --- Computes this character's armor rating. + -- You can override this to return any skill you wish (including non-armor skills, if you so wish). + -- Note that this interface function is read by the engine to update the UI. + -- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything. + -- @function [parent=#Combat] getArmorSkill + -- @param openmw.core#GameObject item The item + -- @return #string The armor skill identifier, or unarmored if the item was nil or not an instace of @{openmw.types#Armor} + getArmorSkill = getArmorSkill, + + --- Computes the armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill + -- Note that this interface function is read by the engine to update the UI. + -- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything. + -- @function [parent=#Combat] getSkillAdjustedArmorRating + -- @param openmw.core#GameObject item The item + -- @param openmw.core#GameObject actor (Optional) The actor, defaults to self + -- @return #number + getSkillAdjustedArmorRating = function(item, actor) return getSkillAdjustedArmorRating(item, actor or self) end, + + --- Computes the effective armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill and item condition + -- @function [parent=#Combat] getEffectiveArmorRating + -- @param openmw.core#GameObject item The item + -- @param openmw.core#GameObject actor (Optional) The actor, defaults to self + -- @return #number + getEffectiveArmorRating = function(item, actor) return getEffectiveArmorRating(item, actor or self) end, + + --- Spawns a random blood effect at the given position + -- @function [parent=#Combat] spawnBloodEffect + -- @param openmw.util#Vector3 position + spawnBloodEffect = spawnBloodEffect, + + --- Hit this actor. Normally called as Hit event from the attacking actor, with the same parameters. + -- @function [parent=#Combat] onHit + -- @param #AttackInfo attackInfo + onHit = onHit, + + --- Picks a random armor slot and returns the item equipped in that slot. + -- Used to pick which armor to damage / skill to increase when hit during combat. + -- @function [parent=#Combat] pickRandomArmor + -- @param openmw.core#GameObject actor (Optional) The actor to pick armor from, defaults to self + -- @return openmw.core#GameObject The armor equipped in the chosen slot. nil if nothing was equipped in that slot. + pickRandomArmor = function(actor) return pickRandomArmor(actor or self) end, + + --- @{#AttackSourceType} + -- @field [parent=#Combat] #AttackSourceType ATTACK_SOURCE_TYPES Available attack source types + ATTACK_SOURCE_TYPES = { + Magic = 'magic', + Melee = 'melee', + Ranged = 'ranged', + Unspecified = 'unspecified', + }, + }, + + eventHandlers = { + Hit = function(data) I.Combat.onHit(data) end, + }, +} diff --git a/files/data/scripts/omw/combat/menu.lua b/files/data/scripts/omw/combat/menu.lua new file mode 100644 index 0000000000..7b6bf2e969 --- /dev/null +++ b/files/data/scripts/omw/combat/menu.lua @@ -0,0 +1 @@ +require('scripts.omw.combat.common').registerSettingsPage() diff --git a/files/data/scripts/omw/mechanics/actorcontroller.lua b/files/data/scripts/omw/mechanics/actorcontroller.lua index fe8c75b244..e4e082263b 100644 --- a/files/data/scripts/omw/mechanics/actorcontroller.lua +++ b/files/data/scripts/omw/mechanics/actorcontroller.lua @@ -19,5 +19,18 @@ return { BreakInvisibility = function(data) Actor.activeEffects(self):remove(core.magic.EFFECT_TYPE.Invisibility) end, + Unequip = function(data) + local equipment = Actor.getEquipment(self) + if data.item then + for slot, item in pairs(equipment) do + if item == data.item then + equipment[slot] = nil + end + end + elseif data.slot then + equipment[slot] = nil + end + Actor.setEquipment(self, equipment) + end, }, } diff --git a/files/data/scripts/omw/mechanics/globalcontroller.lua b/files/data/scripts/omw/mechanics/globalcontroller.lua index 22d92e7c24..c834e6f976 100644 --- a/files/data/scripts/omw/mechanics/globalcontroller.lua +++ b/files/data/scripts/omw/mechanics/globalcontroller.lua @@ -22,6 +22,16 @@ local function onPlaySound3d(data) end end +local function onModifyItemCondition(data) + local itemData = Item.itemData(data.item) + itemData.condition = math.min(data.item.type.record(data.item).health, math.max(0, itemData.condition + data.amount)) + + -- Force unequip broken items + if data.actor and itemData.condition <= 0 then + data.actor:sendEvent('Unequip', {item = data.item}) + end +end + return { eventHandlers = { SpawnVfx = function(data) @@ -35,5 +45,6 @@ return { Unlock = function(data) Lockable.unlock(data.target) end, + ModifyItemCondition = onModifyItemCondition, }, } diff --git a/files/lang/launcher_de.ts b/files/lang/launcher_de.ts index 94437fb822..80e44d7036 100644 --- a/files/lang/launcher_de.ts +++ b/files/lang/launcher_de.ts @@ -690,10 +690,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov <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> - - <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> - - Off @@ -1155,10 +1151,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Classic Reflected Absorb Spells Behavior - - Unarmed Creature Attacks Damage Armor - - Affect Werewolves diff --git a/files/lang/launcher_en.ts b/files/lang/launcher_en.ts index 25717d1454..5b14aa0ade 100644 --- a/files/lang/launcher_en.ts +++ b/files/lang/launcher_en.ts @@ -798,14 +798,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov 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 - - Factor Strength into Hand-to-Hand Combat diff --git a/files/lang/launcher_fr.ts b/files/lang/launcher_fr.ts index 5f8f020981..ad779ac896 100644 --- a/files/lang/launcher_fr.ts +++ b/files/lang/launcher_fr.ts @@ -690,10 +690,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov <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> <html><head/><body><p>Lorsque cette option est activée, le joueur est autorisé à piller créatures et PNJ (p. ex. les créatures invoquées) durant leur animation de mort, si elles ne sont pas en combat. Dans ce cas, le jeu incrémente le conteur de mort et lance son script instantanément.</p><p>Lorsque cette option est désactivée, le joueur doit attendre la fin de l'animation de mort. Dans ce cas, l'utilisation de l'exploit des créatures invoquées (piller des créatures invoquées telles que des Drémoras ou des Saintes Dorées afin d'obtenir des armes de grandes valeurs) est rendu beaucoup plus ardu. Cette option entre en confit avec les Mods de mannequin. En effet, ceux-ci utilisent SkipAnim afin d'éviter la fin de l'animation de mort.</p></body></html> - - <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> - <html><head/><body><p>L'option donne aux créatures non armées la capacité d'endommager les pièces d'armure, comme le font les PNJ et les créatures armées.</p></body></html> - Off Inactif @@ -1158,10 +1154,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Classic Reflected Absorb Spells Behavior Comportement traditionnel de la réflexion des sorts d'absorbtion - - Unarmed Creature Attacks Damage Armor - L'attaque des créatures non armées endomage les armures - Affect Werewolves S'applique aux loups garoux diff --git a/files/lang/launcher_ru.ts b/files/lang/launcher_ru.ts index bf0ea467b2..57789902f7 100644 --- a/files/lang/launcher_ru.ts +++ b/files/lang/launcher_ru.ts @@ -736,14 +736,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov <html><head/><body><p>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.</p></body></html> <html><head/><body><p>Позволяет любым персонажам плыть возле поверхности воды, чтобы следовать за другим персонажем, вне зависимости от того, могут они плыть, или нет. Работает только с навигационной сеткой.</p></body></html> - - <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> - <html><head/><body><p>Позволяет атакам существ без оружия повреждать броню цели, по аналогии с атаками оружием.</p></body></html> - - - Unarmed Creature Attacks Damage Armor - Атаки существ повреждают броню - Off Отключено diff --git a/files/lang/launcher_sv.ts b/files/lang/launcher_sv.ts index 3e6c5607eb..e1903889b7 100644 --- a/files/lang/launcher_sv.ts +++ b/files/lang/launcher_sv.ts @@ -693,10 +693,6 @@ de ordinarie fonterna i Morrowind. Bocka denna ruta om du ändå föredrar ordin <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> <html><head/><body><p>Om denna inställning är aktiv tillåts spelaren plundra figurer (exempelvis tillkallade varelser) under deras dödsanimation, om de inte är i strid.</p><p>Om inställningen är inaktiv måste spelaren vänta tills dödsanimationen är slut. Detta gör det mycket svårare att exploatera tillkallade varelser (exempelvis plundra Draemoror eller Golden Saints för att få dyrbara vapen). Inställningen är i konflikt med skyltdocks-moddar som använder SkipAnim för att förhindra avslutning av dödsanimationer.</p></body></html> - - <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> - <html><head/><body><p>Gör att obeväpnade varelseattacker kan reducera rustningars skick, precis som attacker från icke-spelbara figurer och beväpnade varelser.</p></body></html> - Off Av @@ -1163,10 +1159,6 @@ de ordinarie fonterna i Morrowind. Bocka denna ruta om du ändå föredrar ordin Classic Reflected Absorb Spells Behavior Klassiskt beteende för reflekterade "Absorb"-besvärjelser - - Unarmed Creature Attacks Damage Armor - Obeväpnad attack från varelser skadar rustning - Affect Werewolves Påverka varulvar diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index ceb3829cd3..0567cec859 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -62,6 +62,11 @@ -- @param #string setting Setting name -- @return #any +--- +-- The game's difficulty setting. +-- @function [parent=#core] getGameDifficulty +-- @return #number + --- -- Return l10n formatting function for the given context. -- Localisation files (containing the message names and translations) should be stored in diff --git a/files/lua_api/openmw/interfaces.lua b/files/lua_api/openmw/interfaces.lua index 82db412623..cfdf4728d1 100644 --- a/files/lua_api/openmw/interfaces.lua +++ b/files/lua_api/openmw/interfaces.lua @@ -15,6 +15,9 @@ --- -- @field [parent=#interfaces] scripts.omw.camera.camera#scripts.omw.camera.camera Camera +--- +-- @field [parent=#interfaces] scripts.omw.combat.local#scripts.omw.combat.local Combat + --- -- @field [parent=#interfaces] scripts.omw.mwui.init#scripts.omw.mwui.init MWUI diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index b8c4aebe97..b2711c895b 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -863,6 +863,7 @@ -- @field #boolean isBiped whether the creature is a biped -- @field #boolean isEssential whether the creature is essential -- @field #boolean isRespawning whether the creature respawns after death +-- @field #number bloodType integer representing the blood type of the Creature. Used to generate the correct blood vfx. --- @{#NPC} functions @@ -1137,6 +1138,7 @@ -- @field #list<#TravelDestination> travelDestinations A list of @{#TravelDestination}s for this NPC. -- @field #boolean isEssential whether the NPC is essential -- @field #boolean isRespawning whether the NPC respawns after death +-- @field #number bloodType integer representing the blood type of the NPC. Used to generate the correct blood vfx. --- -- @type TravelDestination diff --git a/files/settings-default.cfg b/files/settings-default.cfg index b9bf113dee..76be5bfc6b 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -363,9 +363,6 @@ default actor pathfind half extents = 29.27999496459961 28.479997634887695 66.5 # Enables use of day/night switch nodes day night switches = true -# Enables degradation of NPC's armor from unarmed creature attacks. -unarmed creature attacks damage armor = false - # Collision is used for both physics simulation and navigation mesh generation for pathfinding: # 0 = Axis-aligned bounding box # 1 = Rotating box